Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 156 additions & 5 deletions core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,31 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated

// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
statedb.SetTxContext(tx.Hash(), i, uint32(i+1))

// Fast path: a bare 21000-gas value transfer to a code-less account has
// a fully deterministic outcome and can be applied without converting the
// transaction to a Message or spinning up the state-transition machinery.
if receipt, bal, ok := tryFastTransfer(tx, signer, gp, statedb, config, blockNumber, blockHash, context, header.BaseFee); ok {
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
blockAccessList.Merge(bal)
continue
}

msg, err := TransactionToMessage(tx, signer, header.BaseFee)
if err != nil {
return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
statedb.SetTxContext(tx.Hash(), i, uint32(i+1))
_, _, spanEnd := telemetry.StartSpan(ctx, "core.ApplyTransactionWithEVM",
telemetry.StringAttribute("tx.hash", tx.Hash().Hex()),
telemetry.IntAttribute("tx.index", i),
)
// Only build the per-tx span (and its attributes, which allocate) when
// tracing is actually active; in the common case it is not.
spanEnd := func(*error) {}
if telemetry.IsRecording(ctx) {
_, _, spanEnd = telemetry.StartSpan(ctx, "core.ApplyTransactionWithEVM",
telemetry.StringAttribute("tx.hash", tx.Hash().Hex()),
telemetry.IntAttribute("tx.index", i),
)
}
receipt, bal, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm)
if err != nil {
spanEnd(&err)
Expand Down Expand Up @@ -220,6 +236,141 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB,
return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), bal, nil
}

// tryFastTransfer handles the common case of a simple value transfer between
// externally-owned accounts without converting the transaction to a Message or
// invoking the EVM. It returns ok=false (and the caller falls back to the full
// state transition) unless every condition below holds, in which case the
// outcome is provably identical to running the message through
// stateTransition.execute:
//
// - the tx is a plain transfer: legacy/dynamic-fee/access-list type, non-nil
// recipient, no calldata, no access list (so intrinsic gas == 21000);
// - the gas limit equals the intrinsic transfer cost (21000), so no gas is
// forwarded to a frame, no refund can arise and the EIP-7623 floor is moot;
// - the chain is pre-Amsterdam / non-Verkle and no tracer is attached, so
// there are no EVM hooks, transfer/burn logs, 2D gas accounting or
// witness-building side effects to reproduce;
// - the sender passes the standard pre-checks (nonce, EOA, fee cap) and can
// afford gas + value;
// - the recipient already exists and carries no code (no contract, no
// delegation), so no account-creation charge and no code execution occur.
//
// The fast path is checked before TransactionToMessage so that the (allocating)
// big.Int -> uint256 conversion of the fee fields is avoided entirely.
func tryFastTransfer(tx *types.Transaction, signer types.Signer, gp *GasPool, statedb *state.StateDB, config *params.ChainConfig, blockNumber *big.Int, blockHash common.Hash, blockCtx vm.BlockContext, baseFee *big.Int) (*types.Receipt, *bal.ConstructionBlockAccessList, bool) {
// Cheap, allocation-free shape checks first. Only plain transfer-shaped
// transactions with the exact intrinsic gas limit qualify.
switch tx.Type() {
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType:
default:
return nil, nil, false
}
to := tx.To()
if to == nil || tx.Gas() != params.TxGas || len(tx.Data()) != 0 || len(tx.AccessList()) != 0 {
return nil, nil, false
}
// The fast path only mirrors the simple pre-Amsterdam, non-Verkle accounting
// and produces no tracer events. Byzantium is required so the receipt carries
// a status flag rather than an intermediate state root.
rules := config.Rules(blockNumber, blockCtx.Random != nil, blockCtx.Time)
if rules.IsAmsterdam || rules.IsEIP4762 || !rules.IsByzantium {
return nil, nil, false
}

// Recover the sender (cached on the tx, and required by the full path too).
from, err := types.Sender(signer, tx)
if err != nil {
return nil, nil, false
}
// Nonce must match exactly; mismatches fall through to surface the error.
if statedb.GetNonce(from) != tx.Nonce() {
return nil, nil, false
}
// Sender must be a plain EOA, recipient must already exist with no code.
if statedb.GetCodeSize(from) != 0 {
return nil, nil, false
}
if statedb.GetCodeSize(*to) != 0 || !statedb.Exist(*to) {
return nil, nil, false
}

// Compute the effective gas price and effective tip, mirroring
// TransactionToMessage and stateTransition.execute. Pre-London the effective
// price is simply the gas price. Under London it is min(feeCap, baseFee+tip),
// the feeCap must be >= the tip and >= the base fee, and the tip paid to the
// coinbase is effectivePrice - baseFee.
feeCap, o1 := uint256.FromBig(tx.GasFeeCap())
tipCap, o2 := uint256.FromBig(tx.GasTipCap())
if o1 || o2 {
return nil, nil, false
}
gasPrice := feeCap
effectiveTip := feeCap
if rules.IsLondon {
base, o3 := uint256.FromBig(baseFee)
if o3 || feeCap.Lt(tipCap) || feeCap.Lt(base) {
return nil, nil, false
}
gasPrice = new(uint256.Int).Add(base, tipCap)
if gasPrice.Gt(feeCap) {
gasPrice = feeCap
}
effectiveTip = new(uint256.Int).Sub(gasPrice, base)
}

value, overflow := uint256.FromBig(tx.Value())
if overflow {
return nil, nil, false
}
// Cost = gas*price + value; the sender must cover all of it.
gasCost := new(uint256.Int).SetUint64(params.TxGas)
if _, o := gasCost.MulOverflow(gasCost, gasPrice); o {
return nil, nil, false
}
total := new(uint256.Int)
if _, o := total.AddOverflow(gasCost, value); o {
return nil, nil, false
}
if statedb.GetBalance(from).Cmp(total) < 0 {
return nil, nil, false
}
// Reserve gas in the block pool; if the block is full, defer to the full path
// so it reports ErrGasLimitReached identically.
if err := gp.CheckGasLegacy(params.TxGas); err != nil {
return nil, nil, false
}

// --- apply (all checks passed; outcome is deterministic) ---
statedb.SetNonce(from, tx.Nonce()+1, tracing.NonceChangeEoACall)
statedb.SubBalance(from, gasCost, tracing.BalanceDecreaseGasBuy)
if !value.IsZero() {
statedb.SubBalance(from, value, tracing.BalanceChangeTransfer)
statedb.AddBalance(*to, value, tracing.BalanceChangeTransfer)
}
// Pay the effective tip (gasPrice - baseFee under London) to the coinbase.
fee := new(uint256.Int).Mul(uint256.NewInt(params.TxGas), effectiveTip)
statedb.AddBalance(blockCtx.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee)

// Charge the block gas pool (no refund, gas used == intrinsic).
if err := gp.ChargeGasLegacy(0, params.TxGas); err != nil {
return nil, nil, false // should be unreachable; fall back to be safe
}
balList := statedb.Finalise(true)

receipt := &types.Receipt{
Type: tx.Type(),
Status: types.ReceiptStatusSuccessful,
CumulativeGasUsed: gp.CumulativeUsed(),
GasUsed: params.TxGas,
TxHash: tx.Hash(),
BlockHash: blockHash,
BlockNumber: blockNumber,
TransactionIndex: uint(statedb.TxIndex()),
}
receipt.Bloom = types.CreateBloom(receipt)
return receipt, balList, true
}

// MakeReceipt generates the receipt object for a transaction given its execution result.
func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, cumulativeGas uint64, root []byte) *types.Receipt {
// Create a new receipt for the transaction, storing the intermediate root
Expand Down
120 changes: 120 additions & 0 deletions core/state_processor_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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"
"crypto/ecdsa"
"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/rawdb"
"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"
)

// benchPureTransferBlock generates a genesis spec and a single block packed
// with as many 21000-gas pure value transfers as fit in the block gas limit.
// All recipients are freshly-derived EOAs with no code, so every transaction
// is a "pure transfer".
func benchPureTransferBlock(b *testing.B, naccounts int) (*Genesis, *types.Block) {
// Derive a set of funded keys.
keys := make([]*ecdsa.PrivateKey, naccounts)
addrs := make([]common.Address, naccounts)
keys[0] = benchRootKey
addrs[0] = benchRootAddr
for i := 1; i < naccounts; i++ {
k, _ := crypto.GenerateKey()
keys[i] = k
addrs[i] = crypto.PubkeyToAddress(k.PublicKey)
}

alloc := make(types.GenesisAlloc, naccounts)
for _, a := range addrs {
alloc[a] = types.Account{Balance: benchRootFunds}
}
gspec := &Genesis{
Config: params.TestChainConfig,
GasLimit: 200_000_000,
Alloc: alloc,
}

from := 0
_, blocks, _ := GenerateChainWithGenesis(gspec, beacon.New(ethash.NewFaker()), 1, func(i int, gen *BlockGen) {
gas := gen.header.GasLimit
gasPrice := big.NewInt(0)
if gen.header.BaseFee != nil {
gasPrice = gen.header.BaseFee
}
signer := gen.Signer()
for gas >= params.TxGas {
gas -= params.TxGas
to := addrs[(from+1)%naccounts]
tx, err := types.SignNewTx(keys[from], signer, &types.LegacyTx{
Nonce: gen.TxNonce(addrs[from]),
To: &to,
Value: big.NewInt(1),
Gas: params.TxGas,
GasPrice: gasPrice,
})
if err != nil {
b.Fatal(err)
}
gen.AddTx(tx)
from = (from + 1) % naccounts
}
})
return gspec, blocks[0]
}

// benchmarkProcess builds a block of pure transfers and times repeated
// Process calls against a fresh state derived from genesis each iteration,
// isolating the cost of execution from disk commit / trie hashing.
func benchmarkProcess(b *testing.B, naccounts int) {
gspec, block := benchPureTransferBlock(b, naccounts)

db := rawdb.NewMemoryDatabase()
chain, err := NewBlockChain(db, gspec, beacon.New(ethash.NewFaker()), nil)
if err != nil {
b.Fatal(err)
}
defer chain.Stop()

processor := chain.Processor()
genesisHeader := chain.Genesis().Header()

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
statedb, err := chain.StateAt(genesisHeader)
if err != nil {
b.Fatal(err)
}
if _, err := processor.Process(context.Background(), block, statedb, nil, vm.Config{}); err != nil {
b.Fatal(err)
}
}
}

func BenchmarkProcessPureTransfers(b *testing.B) {
benchmarkProcess(b, 1000)
}
7 changes: 7 additions & 0 deletions internal/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ func BoolAttribute(key string, val bool) Attribute {
return attribute.Bool(key, val)
}

// IsRecording reports whether the context carries a valid parent span, i.e.
// whether a span started now would actually be exported. Hot paths can use this
// to avoid building span attributes (which may allocate) when tracing is off.
func IsRecording(ctx context.Context) bool {
return trace.SpanFromContext(ctx).SpanContext().IsValid()
}

// StartSpan creates a SpanKind=INTERNAL span.
func StartSpan(ctx context.Context, spanName string, attributes ...Attribute) (context.Context, trace.Span, func(*error)) {
return StartSpanWithTracer(ctx, otel.Tracer(""), spanName, attributes...)
Expand Down
Loading