This commit is contained in:
Emualliug Tellab 2026-03-03 21:54:30 -08:00 committed by GitHub
commit cc16e93193
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1320 additions and 135 deletions

View file

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

View file

@ -23,6 +23,7 @@ import (
"runtime/debug" "runtime/debug"
"github.com/ethereum/go-ethereum/core" "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/stateless"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "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) fmt.Fprintf(os.Stderr, "failed to get chain config: %v\n", err)
os.Exit(13) 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) crossStateRoot, crossReceiptRoot, err := core.ExecuteStateless(context.Background(), chainConfig, vmConfig, payload.Block, payload.Witness)
if err != nil { if err != nil {

View file

@ -621,6 +621,11 @@ var (
Usage: "Generate execution witnesses and self-check against them (testing purpose)", Usage: "Generate execution witnesses and self-check against them (testing purpose)",
Category: flags.VMCategory, 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. // API options.
RPCGlobalGasCapFlag = &cli.Uint64Flag{ RPCGlobalGasCapFlag = &cli.Uint64Flag{
Name: "rpc.gascap", Name: "rpc.gascap",
@ -1902,6 +1907,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if ctx.Bool(VMWitnessStatsFlag.Name) { if ctx.Bool(VMWitnessStatsFlag.Name) {
cfg.StatelessSelfValidation = true cfg.StatelessSelfValidation = true
} }
if ctx.IsSet(VMArenaAllocFlag.Name) {
cfg.EnableArenaAlloc = ctx.Bool(VMArenaAllocFlag.Name)
}
if ctx.IsSet(RPCGlobalGasCapFlag.Name) { if ctx.IsSet(RPCGlobalGasCapFlag.Name) {
cfg.RPCGasCap = ctx.Uint64(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) { 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 := bc.cfg.VmConfig
vmCfg.Tracer = nil vmCfg.Tracer = nil
vmCfg.Allocator = nil
bc.prefetcher.Prefetch(block, throwaway, vmCfg, &interrupt) bc.prefetcher.Prefetch(block, throwaway, vmCfg, &interrupt)
blockPrefetchExecuteTimer.Update(time.Since(start)) blockPrefetchExecuteTimer.Update(time.Since(start))

View file

@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844" "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/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
@ -39,6 +40,12 @@ type ChainContext interface {
// NewEVMBlockContext creates a new context for use in the EVM. // NewEVMBlockContext creates a new context for use in the EVM.
func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext { 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 ( var (
beneficiary common.Address beneficiary common.Address
baseFee *big.Int baseFee *big.Int
@ -65,15 +72,17 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
if header.SlotNumber != nil { if header.SlotNumber != nil {
slotNum = *header.SlotNumber slotNum = *header.SlotNumber
} }
blockNumber := new(big.Int).Set(header.Number)
difficulty := new(big.Int).Set(header.Difficulty)
return vm.BlockContext{ return vm.BlockContext{
CanTransfer: CanTransfer, CanTransfer: CanTransfer,
Transfer: Transfer, Transfer: Transfer,
GetHash: GetHashFn(header, chain), GetHash: GetHashFn(header, chain),
Coinbase: beneficiary, Coinbase: beneficiary,
BlockNumber: new(big.Int).Set(header.Number), BlockNumber: blockNumber,
Time: header.Time, Time: header.Time,
Difficulty: new(big.Int).Set(header.Difficulty), Difficulty: difficulty,
BaseFee: baseFee, BaseFee: baseFee,
BlobBaseFee: blobBaseFee, BlobBaseFee: blobBaseFee,
GasLimit: header.GasLimit, 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. // NewEVMTxContext creates a new transaction context for a single transaction.
func NewEVMTxContext(msg *Message) vm.TxContext { 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{ ctx := vm.TxContext{
Origin: msg.From, Origin: msg.From,
GasPrice: uint256.MustFromBig(msg.GasPrice), GasPrice: new(uint256.Int).Set(&msg.GasPrice),
BlobHashes: msg.BlobHashes, BlobHashes: msg.BlobHashes,
} }
return ctx return ctx

View file

@ -23,13 +23,16 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc" "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/state"
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/telemetry" "github.com/ethereum/go-ethereum/internal/telemetry"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
) )
// StateProcessor is a basic Processor, which takes care of transitioning // 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() blockNumber = block.Number()
allLogs []*types.Log allLogs []*types.Log
gp = new(GasPool).AddGas(block.GasLimit()) 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) var tracingStateDB = vm.StateDB(statedb)
if hooks := cfg.Tracer; hooks != nil { 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. // 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) evm := vm.NewEVM(context, tracingStateDB, config, cfg)
if beaconRoot := block.BeaconRoot(); beaconRoot != nil { 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 // Iterate over and process the individual transactions
for i, tx := range block.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 { if err != nil {
return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) 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{ msg := &Message{
From: params.SystemAddress, From: params.SystemAddress,
GasLimit: 30_000_000, GasLimit: 30_000_000,
GasPrice: common.Big0, GasPrice: uint256.Int{},
GasFeeCap: common.Big0, GasFeeCap: uint256.Int{},
GasTipCap: common.Big0, GasTipCap: uint256.Int{},
To: &params.BeaconRootsAddress, To: &params.BeaconRootsAddress,
Data: beaconRoot[:], Data: beaconRoot[:],
} }
@ -271,9 +281,9 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) {
msg := &Message{ msg := &Message{
From: params.SystemAddress, From: params.SystemAddress,
GasLimit: 30_000_000, GasLimit: 30_000_000,
GasPrice: common.Big0, GasPrice: uint256.Int{},
GasFeeCap: common.Big0, GasFeeCap: uint256.Int{},
GasTipCap: common.Big0, GasTipCap: uint256.Int{},
To: &params.HistoryStorageAddress, To: &params.HistoryStorageAddress,
Data: prevHash.Bytes(), Data: prevHash.Bytes(),
} }
@ -311,9 +321,9 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte
msg := &Message{ msg := &Message{
From: params.SystemAddress, From: params.SystemAddress,
GasLimit: 30_000_000, GasLimit: 30_000_000,
GasPrice: common.Big0, GasPrice: uint256.Int{},
GasFeeCap: common.Big0, GasFeeCap: uint256.Int{},
GasTipCap: common.Big0, GasTipCap: uint256.Int{},
To: &addr, To: &addr,
} }
evm.SetTxContext(NewEVMTxContext(msg)) evm.SetTxContext(NewEVMTxContext(msg))

View file

@ -23,6 +23,7 @@ import (
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common" "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/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
@ -146,14 +147,14 @@ type Message struct {
To *common.Address To *common.Address
From common.Address From common.Address
Nonce uint64 Nonce uint64
Value *big.Int Value uint256.Int
GasLimit uint64 GasLimit uint64
GasPrice *big.Int GasPrice uint256.Int
GasFeeCap *big.Int GasFeeCap uint256.Int
GasTipCap *big.Int GasTipCap uint256.Int
Data []byte Data []byte
AccessList types.AccessList AccessList types.AccessList
BlobGasFeeCap *big.Int BlobGasFeeCap uint256.Int
BlobHashes []common.Hash BlobHashes []common.Hash
SetCodeAuthorizations []types.SetCodeAuthorization SetCodeAuthorizations []types.SetCodeAuthorization
@ -174,32 +175,68 @@ type Message struct {
// TransactionToMessage converts a transaction into a Message. // TransactionToMessage converts a transaction into a Message.
func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) { func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) {
msg := &Message{ return TransactionToMessageWithAlloc(tx, s, baseFee, arena.DefaultHeap)
Nonce: tx.Nonce(), }
GasLimit: tx.Gas(),
GasPrice: tx.GasPrice(), // TransactionToMessageWithAlloc converts a transaction into a Message, using
GasFeeCap: tx.GasFeeCap(), // the provided allocator for the Message struct and its transient big.Int fields.
GasTipCap: tx.GasTipCap(), func TransactionToMessageWithAlloc(tx *types.Transaction, s types.Signer, baseFee *big.Int, alloc arena.Allocator) (*Message, error) {
To: tx.To(), msg := arena.New[Message](alloc)
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
}
}
var err error var err error
msg.From, err = types.Sender(s, tx) 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 // 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 // indicates a core error meaning that the message would always fail for that particular
// state and would never be accepted within a block. // state and would never be accepted within a block.
func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, error) { 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() return newStateTransition(evm, msg, gp).execute()
} }
@ -243,16 +281,19 @@ type stateTransition struct {
initialGas uint64 initialGas uint64
state vm.StateDB state vm.StateDB
evm *vm.EVM evm *vm.EVM
alloc arena.Allocator
} }
// newStateTransition initialises and returns a new state transition object. // newStateTransition initialises and returns a new state transition object.
func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition { func newStateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *stateTransition {
return &stateTransition{ alloc := evm.Config.GetAllocator()
gp: gp, st := new(stateTransition)
evm: evm, st.gp = gp
msg: msg, st.evm = evm
state: evm.StateDB, st.msg = msg
} st.state = evm.StateDB
st.alloc = alloc
return st
} }
// to returns the recipient of the message. // to returns the recipient of the message.
@ -264,20 +305,21 @@ func (st *stateTransition) to() common.Address {
} }
func (st *stateTransition) buyGas() error { 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 := 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) balanceCheck := new(big.Int).Set(mgval)
if st.msg.GasFeeCap != nil { if !st.msg.GasFeeCap.IsZero() {
balanceCheck.SetUint64(st.msg.GasLimit) 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 st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
if blobGas := st.blobGasUsed(); blobGas > 0 { if blobGas := st.blobGasUsed(); blobGas > 0 {
// Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap // Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap
blobBalanceCheck := new(big.Int).SetUint64(blobGas) blobBalanceCheck := new(big.Int).SetUint64(blobGas)
blobBalanceCheck.Mul(blobBalanceCheck, st.msg.BlobGasFeeCap) blobBalanceCheck.Mul(blobBalanceCheck, st.msg.BlobGasFeeCap.ToBig())
balanceCheck.Add(balanceCheck, blobBalanceCheck) balanceCheck.Add(balanceCheck, blobBalanceCheck)
// Pay for blobGasUsed * actual blob fee // Pay for blobGasUsed * actual blob fee
blobFee := new(big.Int).SetUint64(blobGas) 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) // Make sure that transaction gasFeeCap is greater than the baseFee (post london)
if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) { if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
// Skip the checks if gas fields are zero and baseFee was explicitly disabled (eth_call) // 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 !skipCheck {
if l := msg.GasFeeCap.BitLen(); l > 256 { if msg.GasFeeCap.Cmp(&msg.GasTipCap) < 0 {
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 {
return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap, 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 // This will panic if baseFee is nil, but basefee presence is verified
// as part of header validation. // 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, 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.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
if st.blobGasUsed() > 0 { if st.blobGasUsed() > 0 {
// Skip the checks if gas fields are zero and blobBaseFee was explicitly disabled (eth_call) // 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 { if !skipCheck {
// This will panic if blobBaseFee is nil, but blobBaseFee presence // This will panic if blobBaseFee is nil, but blobBaseFee presence
// is verified as part of header validation. // 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, return fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow,
msg.From.Hex(), msg.BlobGasFeeCap, st.evm.Context.BlobBaseFee) msg.From.Hex(), msg.BlobGasFeeCap, st.evm.Context.BlobBaseFee)
} }
@ -474,11 +510,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
} }
// Check clause 6 // Check clause 6
value, overflow := uint256.FromBig(msg.Value) if !msg.Value.IsZero() && !st.evm.Context.CanTransfer(st.state, msg.From, &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) {
return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex()) 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 vmerr error // vm errors do not effect consensus and are therefore not assigned to err
) )
if contractCreation { 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 { } else {
// Increment the nonce for the next transaction. // Increment the nonce for the next transaction.
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) 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. // 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 // Record the gas used excluding gas refunds. This value represents the actual
@ -544,13 +576,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
} }
st.returnGas() st.returnGas()
effectiveTip := msg.GasPrice effectiveTipU256 := new(uint256.Int).Set(&msg.GasPrice)
if rules.IsLondon { 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 // Skip fee payment when NoBaseFee is set and the fee fields
// are 0. This avoids a negative effectiveTip being applied to // are 0. This avoids a negative effectiveTip being applied to
// the coinbase when simulating calls. // the coinbase when simulating calls.
@ -565,12 +597,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
} }
} }
return &ExecutionResult{ result := new(ExecutionResult)
UsedGas: st.gasUsed(), result.UsedGas = st.gasUsed()
MaxUsedGas: peakGasUsed, result.MaxUsedGas = peakGasUsed
Err: vmerr, result.Err = vmerr
ReturnData: ret, result.ReturnData = ret
}, nil return result, nil
} }
// validateAuthorization validates an EIP-7702 authorization against the state. // validateAuthorization validates an EIP-7702 authorization against the state.
@ -654,7 +686,7 @@ func (st *stateTransition) calcRefund() uint64 {
// exchanged at the original rate. // exchanged at the original rate.
func (st *stateTransition) returnGas() { func (st *stateTransition) returnGas() {
remaining := uint256.NewInt(st.gasRemaining) 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) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn)
if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining > 0 { if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining > 0 {

View file

@ -18,6 +18,7 @@ package vm
import ( import (
"github.com/ethereum/go-ethereum/common" "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/tracing"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
@ -48,17 +49,23 @@ type Contract struct {
// NewContract returns a new contract environment for the execution of EVM. // 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 { 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 // Initialize the jump analysis cache if it's nil, mostly for tests
if jumpDests == nil { if jumpDests == nil {
jumpDests = newMapJumpDests() jumpDests = newMapJumpDests()
} }
return &Contract{ c := new(Contract)
caller: caller, c.caller = caller
address: address, c.address = address
jumpDests: jumpDests, c.jumpDests = jumpDests
Gas: gas, c.Gas = gas
value: value, c.value = value
} return c
} }
func (c *Contract) validJumpdest(dest *uint256.Int) bool { func (c *Contract) validJumpdest(dest *uint256.Int) bool {

View file

@ -22,6 +22,7 @@ import (
"sync/atomic" "sync/atomic"
"github.com/ethereum/go-ethereum/common" "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/state"
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types" "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 ret, err = nil, nil // gas is unchanged
} else { } else {
// The contract is a scoped environment for this execution context only. // 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.IsSystemCall = isSystemCall(caller)
contract.SetCallCode(evm.resolveCodeHash(addr), code) contract.SetCallCode(evm.resolveCodeHash(addr), code)
ret, err = evm.Run(contract, input, false) ret, err = evm.Run(contract, input, false)
@ -361,7 +363,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
} else { } else {
// Initialise a new contract and set the code that is to be used by the EVM. // 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. // 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)) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false) ret, err = evm.Run(contract, input, false)
gas = contract.Gas 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 // Initialise a new contract and make initialise the delegate values
// //
// Note: The value refers to the original value from the parent call. // 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)) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false) ret, err = evm.Run(contract, input, false)
gas = contract.Gas gas = contract.Gas
@ -464,7 +468,9 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
} else { } else {
// Initialise a new contract and set the code that is to be used by the EVM. // 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. // 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)) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
// When an error was returned by the EVM or when setting the creation code // 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. // 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. // 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 // Explicitly set the code to a null hash to prevent caching of jump analysis
// for the initialization code. // for the initialization code.

View file

@ -21,6 +21,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/arena"
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
@ -28,14 +29,24 @@ import (
// Config are the configuration options for the Interpreter // Config are the configuration options for the Interpreter
type Config struct { type Config struct {
Tracer *tracing.Hooks Tracer *tracing.Hooks
NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)
EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages
ExtraEips []int // Additional EIPS that are to be enabled 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) StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose)
EnableWitnessStats bool // Whether trie access statistics collection is enabled 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, // ScopeContext contains the things that are per-call, such as stack and memory,
// but not transients like pc and gas // but not transients like pc and gas
type ScopeContext struct { type ScopeContext struct {
@ -115,11 +126,12 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
return nil, nil return nil, nil
} }
alloc := evm.Config.GetAllocator()
var ( var (
op OpCode // current opcode op OpCode // current opcode
jumpTable *JumpTable = evm.table jumpTable *JumpTable = evm.table
mem = NewMemory() // bound memory mem = NewMemoryWithAlloc(alloc) // bound memory
stack = newstack() // local stack stack = newstackWithAlloc(alloc) // local stack
callContext = &ScopeContext{ callContext = &ScopeContext{
Memory: mem, Memory: mem,
Stack: stack, Stack: stack,

View file

@ -19,6 +19,7 @@ package vm
import ( import (
"sync" "sync"
"github.com/ethereum/go-ethereum/core/arena"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
@ -32,6 +33,7 @@ var memoryPool = sync.Pool{
type Memory struct { type Memory struct {
store []byte store []byte
lastGasCost uint64 lastGasCost uint64
arenaAlloc bool // true if allocated from a BumpAllocator (skip pool return)
} }
// NewMemory returns a new memory model. // NewMemory returns a new memory model.
@ -39,8 +41,23 @@ func NewMemory() *Memory {
return memoryPool.Get().(*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. // Free returns the memory to the pool.
func (m *Memory) Free() { 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. // To reduce peak allocation, return only smaller memory instances to the pool.
const maxBufferSize = 16 << 10 const maxBufferSize = 16 << 10
if cap(m.store) <= maxBufferSize { 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 ( import (
"sync" "sync"
"github.com/ethereum/go-ethereum/core/arena"
"github.com/holiman/uint256" "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 // expected to be changed and modified. stack does not take care of adding newly
// initialized objects. // initialized objects.
type Stack struct { type Stack struct {
data []uint256.Int data []uint256.Int
arenaAlloc bool // true if allocated from a BumpAllocator (skip pool return)
} }
func newstack() *Stack { func newstack() *Stack {
return stackPool.Get().(*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) { func returnStack(s *Stack) {
if s.arenaAlloc {
return // arena-allocated: freed on arena.Reset()
}
s.data = s.data[:0] s.data = s.data[:0]
stackPool.Put(s) stackPool.Put(s)
} }

View file

@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core" "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/filtermaps"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/pruner" "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, EnablePreimageRecording: config.EnablePreimageRecording,
EnableWitnessStats: config.EnableWitnessStats, EnableWitnessStats: config.EnableWitnessStats,
StatelessSelfValidation: config.StatelessSelfValidation, StatelessSelfValidation: config.StatelessSelfValidation,
Allocator: newAllocatorFromConfig(config),
}, },
// Enables file journaling for the trie database. The journal files will be stored // Enables file journaling for the trie database. The journal files will be stored
// within the data directory. The corresponding paths will be either: // within the data directory. The corresponding paths will be either:
@ -597,3 +599,12 @@ func (s *Ethereum) Stop() error {
return nil 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) // Generate execution witnesses and self-check against them (testing purpose)
StatelessSelfValidation bool StatelessSelfValidation bool
// Use bump/arena allocator for transient EVM allocations during block processing
EnableArenaAlloc bool
// Enables tracking of state size // Enables tracking of state size
EnableStateSizeTracking bool EnableStateSizeTracking bool

View file

@ -55,6 +55,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
EnablePreimageRecording bool EnablePreimageRecording bool
EnableWitnessStats bool EnableWitnessStats bool
StatelessSelfValidation bool StatelessSelfValidation bool
EnableArenaAlloc bool
EnableStateSizeTracking bool EnableStateSizeTracking bool
VMTrace string VMTrace string
VMTraceJsonConfig string VMTraceJsonConfig string
@ -108,6 +109,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
enc.EnablePreimageRecording = c.EnablePreimageRecording enc.EnablePreimageRecording = c.EnablePreimageRecording
enc.EnableWitnessStats = c.EnableWitnessStats enc.EnableWitnessStats = c.EnableWitnessStats
enc.StatelessSelfValidation = c.StatelessSelfValidation enc.StatelessSelfValidation = c.StatelessSelfValidation
enc.EnableArenaAlloc = c.EnableArenaAlloc
enc.EnableStateSizeTracking = c.EnableStateSizeTracking enc.EnableStateSizeTracking = c.EnableStateSizeTracking
enc.VMTrace = c.VMTrace enc.VMTrace = c.VMTrace
enc.VMTraceJsonConfig = c.VMTraceJsonConfig enc.VMTraceJsonConfig = c.VMTraceJsonConfig
@ -165,6 +167,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
EnablePreimageRecording *bool EnablePreimageRecording *bool
EnableWitnessStats *bool EnableWitnessStats *bool
StatelessSelfValidation *bool StatelessSelfValidation *bool
EnableArenaAlloc *bool
EnableStateSizeTracking *bool EnableStateSizeTracking *bool
VMTrace *string VMTrace *string
VMTraceJsonConfig *string VMTraceJsonConfig *string
@ -297,6 +300,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
if dec.StatelessSelfValidation != nil { if dec.StatelessSelfValidation != nil {
c.StatelessSelfValidation = *dec.StatelessSelfValidation c.StatelessSelfValidation = *dec.StatelessSelfValidation
} }
if dec.EnableArenaAlloc != nil {
c.EnableArenaAlloc = *dec.EnableArenaAlloc
}
if dec.EnableStateSizeTracking != nil { if dec.EnableStateSizeTracking != nil {
c.EnableStateSizeTracking = *dec.EnableStateSizeTracking c.EnableStateSizeTracking = *dec.EnableStateSizeTracking
} }

View file

@ -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. // Normalize the max fee per gas the call is willing to spend.
var feeCap *big.Int var feeCap *big.Int
if call.GasFeeCap != nil { if !call.GasFeeCap.IsZero() {
feeCap = call.GasFeeCap feeCap = call.GasFeeCap.ToBig()
} else if call.GasPrice != nil { } else if !call.GasPrice.IsZero() {
feeCap = call.GasPrice feeCap = call.GasPrice.ToBig()
} else { } else {
feeCap = common.Big0 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() balance := opts.State.GetBalance(call.From).ToBig()
available := balance available := balance
if call.Value != nil { if !call.Value.IsZero() {
if call.Value.Cmp(available) >= 0 { valueBig := call.Value.ToBig()
if valueBig.Cmp(available) >= 0 {
return 0, nil, core.ErrInsufficientFundsForTransfer 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 { if opts.Config.IsCancun(opts.Header.Number, opts.Header.Time) && len(call.BlobHashes) > 0 {
blobGasPerBlob := new(big.Int).SetInt64(params.BlobTxBlobGasPerBlob) blobGasPerBlob := new(big.Int).SetInt64(params.BlobTxBlobGasPerBlob)
blobBalanceUsage := new(big.Int).SetInt64(int64(len(call.BlobHashes))) blobBalanceUsage := new(big.Int).SetInt64(int64(len(call.BlobHashes)))
blobBalanceUsage.Mul(blobBalanceUsage, blobGasPerBlob) blobBalanceUsage.Mul(blobBalanceUsage, blobGasPerBlob)
blobBalanceUsage.Mul(blobBalanceUsage, call.BlobGasFeeCap) blobBalanceUsage.Mul(blobBalanceUsage, call.BlobGasFeeCap.ToBig())
if blobBalanceUsage.Cmp(available) >= 0 { if blobBalanceUsage.Cmp(available) >= 0 {
return 0, nil, core.ErrInsufficientFunds 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 the allowance is larger than maximum uint64, skip checking
if allowance.IsUint64() && hi > allowance.Uint64() { if allowance.IsUint64() && hi > allowance.Uint64() {
transfer := call.Value transfer := new(big.Int)
if transfer == nil { if !call.Value.IsZero() {
transfer = new(big.Int) transfer = call.Value.ToBig()
} }
log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance, log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance,
"sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance) "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 { if call.GasPrice.Sign() == 0 {
evmContext.BaseFee = new(big.Int) evmContext.BaseFee = new(big.Int)
} }
if call.BlobGasFeeCap != nil && call.BlobGasFeeCap.BitLen() == 0 { if call.BlobGasFeeCap.IsZero() {
evmContext.BlobBaseFee = new(big.Int) evmContext.BlobBaseFee = new(big.Int)
} }
evm := vm.NewEVM(evmContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true}) evm := vm.NewEVM(evmContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true})

View file

@ -994,7 +994,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
if msg.GasPrice.Sign() == 0 { if msg.GasPrice.Sign() == 0 {
blockContext.BaseFee = new(big.Int) 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) blockContext.BlobBaseFee = new(big.Int)
} }
if config != nil { if config != nil {

View file

@ -761,7 +761,7 @@ func applyMessage(ctx context.Context, b Backend, args TransactionArgs, state *s
if msg.GasPrice.Sign() == 0 { if msg.GasPrice.Sign() == 0 {
blockContext.BaseFee = new(big.Int) 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) blockContext.BlobBaseFee = new(big.Int)
} }
evm := b.GetEVM(ctx, state, header, vmConfig, blockContext) 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 { if msg.GasPrice.Sign() == 0 {
evm.Context.BaseFee = new(big.Int) 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) evm.Context.BlobBaseFee = new(big.Int)
} }
res, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit)) res, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))

View file

@ -477,18 +477,25 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c
if args.AccessList != nil { if args.AccessList != nil {
accessList = *args.AccessList 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{ return &core.Message{
From: args.from(), From: args.from(),
To: args.To, To: args.To,
Value: (*big.Int)(args.Value), Value: bigToU256((*big.Int)(args.Value)),
Nonce: uint64(*args.Nonce), Nonce: uint64(*args.Nonce),
GasLimit: uint64(*args.Gas), GasLimit: uint64(*args.Gas),
GasPrice: gasPrice, GasPrice: bigToU256(gasPrice),
GasFeeCap: gasFeeCap, GasFeeCap: bigToU256(gasFeeCap),
GasTipCap: gasTipCap, GasTipCap: bigToU256(gasTipCap),
Data: args.data(), Data: args.data(),
AccessList: accessList, AccessList: accessList,
BlobGasFeeCap: (*big.Int)(args.BlobFeeCap), BlobGasFeeCap: bigToU256((*big.Int)(args.BlobFeeCap)),
BlobHashes: args.BlobHashes, BlobHashes: args.BlobHashes,
SetCodeAuthorizations: args.AuthorizationList, SetCodeAuthorizations: args.AuthorizationList,
SkipNonceChecks: skipNonceCheck, SkipNonceChecks: skipNonceCheck,

BIN
keeper-zisk.elf Normal file

Binary file not shown.

View file

@ -35,7 +35,6 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/holiman/uint256"
) )
func initMatcher(st *testMatcher) { func initMatcher(st *testMatcher) {
@ -316,7 +315,7 @@ func runBenchmark(b *testing.B, t *StateTest) {
start := time.Now() start := time.Now()
// Execute the message. // 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 { if err != nil {
b.Error(err) b.Error(err)
return return

View file

@ -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{ msg := &core.Message{
From: from, From: from,
To: to, To: to,
Nonce: tx.Nonce, Nonce: tx.Nonce,
Value: value, Value: *uint256.MustFromBig(value),
GasLimit: gasLimit, GasLimit: gasLimit,
GasPrice: gasPrice, GasPrice: *uint256.MustFromBig(gasPrice),
GasFeeCap: tx.MaxFeePerGas, GasFeeCap: *uint256.MustFromBig(tx.MaxFeePerGas),
GasTipCap: tx.MaxPriorityFeePerGas, GasTipCap: *uint256.MustFromBig(tx.MaxPriorityFeePerGas),
Data: data, Data: data,
AccessList: accessList, AccessList: accessList,
BlobHashes: tx.BlobVersionedHashes, BlobHashes: tx.BlobVersionedHashes,
BlobGasFeeCap: tx.BlobGasFeeCap, BlobGasFeeCap: blobGasFeeCap,
SetCodeAuthorizations: authList, SetCodeAuthorizations: authList,
} }
return msg, nil return msg, nil