Skip to content
Merged
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ require (
github.com/consensys/gnark-crypto v0.19.2
github.com/crate-crypto/go-kzg-4844 v1.1.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/erigontech/secp256k1 v1.2.1-0.20260218182123-377cc1bd6410
github.com/holiman/uint256 v1.3.2
golang.org/x/crypto v0.48.0
)

require (
github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/erigontech/secp256k1 v1.2.1-0.20260218182123-377cc1bd6410 h1:5YD7JJ5PaqOdjKA84lTDtQby9nbI4podqrkUhyIyFDw=
github.com/erigontech/secp256k1 v1.2.1-0.20260218182123-377cc1bd6410/go.mod h1:CnigLRUsfobnTYoUzwdT/De67fw9OhTA0KTkxa+svik=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
Expand Down
147 changes: 111 additions & 36 deletions host/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ type Evm struct {
Cfg CfgEnv
ForkID spec.ForkID
Runner vm.Runner // pluggable interpreter (nil = DefaultRunner)
Hooks *vm.Hooks // lifecycle hooks without per-opcode tracing
ReturnAlloc vm.ReturnDataArena // arena for pooling RETURN/REVERT output buffers
host EvmHost // reusable host embedded in pooled Evm (avoids heap escape)
JumpTableCache map[types.B256][]byte // cached JUMPDEST bitmaps, persists across pooled reuse
Expand Down Expand Up @@ -166,10 +167,15 @@ func (evm *Evm) Set(runner vm.Runner) {
evm.Runner = runner
}

func (evm *Evm) SetHooks(hooks *vm.Hooks) {
evm.Hooks = hooks
}

// ReleaseEvm returns the Evm and its Journal to their pools.
func (evm *Evm) ReleaseEvm() {
evm.ReturnAlloc.Reset()
evm.Runner = nil
evm.Hooks = nil
if evm.Journal != nil {
state.ReleaseJournal(evm.Journal)
evm.Journal = nil
Expand All @@ -180,8 +186,34 @@ func (evm *Evm) ReleaseEvm() {
// MaxInitCodeSize is the maximum initcode size (EIP-3860): 2 * MaxCodeSize.
const MaxInitCodeSize = 2 * MaxCodeSize

// Transact executes a transaction and returns the result.
func (evm *Evm) Transact(tx *Transaction) ExecutionResult {
return evm.transact(tx, true)
}

// TransactInPlace executes a transaction, writing the result Output and Logs
// into caller-provided buffers. Both buffers are truncated to zero length and
// then appended to (growing if needed); the (possibly grown) buffers are
// returned for the caller to reuse on the next call.
//
// The slices in each returned Log's Data field are still BORROWED from
// Evm-owned storage — the caller must consume them before the next
// Transact / TransactInPlace / CommitTx / ReleaseEvm call. To detach Log.Data
// from Evm storage, use Transact (which deep-copies).
//
// Use this in batch execution paths (block runners, EEST replay) where the
// caller persists output + logs immediately after each tx and would otherwise
// pay the per-tx allocation cost of Transact's safe copies.
func (evm *Evm) TransactInPlace(tx *Transaction, outBuf []byte, logsBuf []state.Log) (ExecutionResult, []byte, []state.Log) {
result := evm.transact(tx, false)
outBuf = append(outBuf[:0], result.Output...)
logsBuf = append(logsBuf[:0], result.Logs...)
result.Output = outBuf
result.Logs = logsBuf
return result, outBuf, logsBuf
}

// transact executes a transaction and returns the result.
func (evm *Evm) transact(tx *Transaction, copyResult bool) ExecutionResult {
haltResult := func(reason vm.InstructionResult) ExecutionResult {
return ExecutionResult{
Kind: ResultHalt,
Expand All @@ -191,6 +223,10 @@ func (evm *Evm) Transact(tx *Transaction) ExecutionResult {
}
}

evm.host.Block = &evm.Block
evm.host.Cfg = &evm.Cfg
evm.host.Journal = evm.Journal

// --- Phase 1: Validation ---

// Validate tx type is supported at the current fork
Expand Down Expand Up @@ -323,8 +359,14 @@ func (evm *Evm) Transact(tx *Transaction) ExecutionResult {

// EIP-3607: Reject transactions from senders with deployed code
// EIP-7702: allow senders with delegation code (0xef0100 || address)
if evm.ForkID.IsEnabledIn(spec.Shanghai) && len(callerAcc.Info.Code) > 0 && !IsEIP7702Bytecode(callerAcc.Info.Code) {
return haltResult(vm.InstructionResultSenderNotEOA)
if evm.ForkID.IsEnabledIn(spec.Shanghai) && callerAcc.Info.CodeHash != types.KeccakEmpty && !callerAcc.Info.CodeHash.IsZero() {
callerCode, err := evm.host.loadCode(tx.Caller, callerAcc)
if err != nil {
return haltResult(vm.InstructionResultFatalExternalError)
}
if len(callerCode) > 0 && !IsEIP7702Bytecode(callerCode) {
return haltResult(vm.InstructionResultSenderNotEOA)
}
}

// Nonce check
Expand Down Expand Up @@ -390,11 +432,7 @@ func (evm *Evm) Transact(tx *Transaction) ExecutionResult {
}

// Reuse host embedded in pooled Evm struct (no heap escape).
// Block and Cfg stored by pointer to avoid ~316 bytes of duffcopy.
evm.host.Block = &evm.Block
evm.host.Tx = TxEnv{Caller: tx.Caller, EffectiveGasPrice: effectiveGasPrice, BlobHashes: tx.BlobHashes}
evm.host.Cfg = &evm.Cfg
evm.host.Journal = evm.Journal

// Use embedded root memory (avoids memoryPool Get/Put).
vm.InitEmbeddedMemory(&evm.rootMemory)
Expand All @@ -413,6 +451,7 @@ func (evm *Evm) Transact(tx *Transaction) ExecutionResult {
precompileSet := precompiles.ForSpec(evm.ForkID)
evm.host.Precompiles = precompileSet
evm.host.DisablePrecompileFastPath = false
evm.host.Hooks = nil
handler := Handler{
Host: &evm.host,
Precompiles: precompileSet,
Expand All @@ -423,12 +462,12 @@ func (evm *Evm) Transact(tx *Transaction) ExecutionResult {
RootBytecode: &evm.rootBytecode,
Runner: runner,
}
// Extract lifecycle hooks from TracingRunner
if tr, ok := runner.(*vm.TracingRunner); ok {
if evm.Hooks != nil {
handler.hooks = evm.Hooks
evm.host.Hooks = evm.Hooks
} else if tr, ok := runner.(*vm.TracingRunner); ok {
handler.hooks = tr.Hooks
if tr.Hooks != nil && (tr.Hooks.OnEnter != nil || tr.Hooks.OnExit != nil) {
evm.host.DisablePrecompileFastPath = true
}
evm.host.Hooks = tr.Hooks
}

// Warm precompile addresses via shared map pointer (no Account objects created).
Expand All @@ -443,9 +482,9 @@ func (evm *Evm) Transact(tx *Transaction) ExecutionResult {

// Warm access list addresses and storage keys (EIP-2930+)
for _, item := range tx.AccessList {
_, _ = evm.Journal.LoadAccount(item.Address)
evm.Journal.WarmAddresses.AddAccessListAddress(item.Address)
for _, key := range item.StorageKeys {
_, _ = evm.Journal.SLoad(item.Address, key)
evm.Journal.WarmAddresses.AddAccessListStorage(item.Address, key)
}
}

Expand Down Expand Up @@ -541,30 +580,41 @@ func (evm *Evm) Transact(tx *Transaction) ExecutionResult {
reimburseGasU := uint256.Int{reimburseGas, 0, 0, 0}
var refundAmount uint256.Int
refundAmount.Mul(&effectiveGasPrice, &reimburseGasU)
callerReload, _ := evm.Journal.LoadAccount(tx.Caller)
callerReload.Data.Info.Balance.Add(&callerReload.Data.Info.Balance, &refundAmount)
if err := evm.Journal.BalanceIncr(tx.Caller, refundAmount); err != nil {
return haltResult(vm.InstructionResultFatalExternalError)
}

// Step 4: Reward beneficiary (coinbase).
// Uses gas.Used() = gas.Spent() - refunded (includes intrinsic gas).
evm.rewardBeneficiary(effectiveGasPrice, gas.Used())

// Build execution result. Copy Output and Logs so the result is safe
// to hold after ReleaseEvm (both alias pooled memory).
// Build execution result. Public Transact copies Output and Logs so
// the result is safe to hold after ReleaseEvm; TransactInPlace runs
// this borrowed path and then re-points the result slices at
// caller-provided buffers.
var output types.Bytes
if len(interpResult.Output) > 0 {
output = make(types.Bytes, len(interpResult.Output))
copy(output, interpResult.Output)
if copyResult {
output = make(types.Bytes, len(interpResult.Output))
copy(output, interpResult.Output)
} else {
output = interpResult.Output
}
}
var logs []state.Log
if len(evm.Journal.Logs) > 0 {
logs = make([]state.Log, len(evm.Journal.Logs))
copy(logs, evm.Journal.Logs)
for i := range logs {
if len(logs[i].Data) > 0 {
data := make(types.Bytes, len(logs[i].Data))
copy(data, logs[i].Data)
logs[i].Data = data
if copyResult {
logs = make([]state.Log, len(evm.Journal.Logs))
copy(logs, evm.Journal.Logs)
for i := range logs {
if len(logs[i].Data) > 0 {
data := make(types.Bytes, len(logs[i].Data))
copy(data, logs[i].Data)
logs[i].Data = data
}
}
} else {
logs = evm.Journal.Logs
}
}
result := ExecutionResult{
Expand Down Expand Up @@ -705,18 +755,16 @@ func (evm *Evm) rewardBeneficiary(gasPrice uint256.Int, gasUsed uint64) {
var reward uint256.Int
reward.Mul(&effectivePrice, &gasUsedU)

// Skip loading the beneficiary account if reward is zero.
// LoadAccount without a subsequent touch has no observable state effect.
if reward.IsZero() {
load, err := evm.Journal.LoadAccount(evm.Block.Beneficiary)
if err == nil && !load.Data.IsLoadedAsNotExisting() {
evm.Journal.Touch(evm.Block.Beneficiary)
}
return
}

beneficiary := evm.Block.Beneficiary
beneficiaryResult, err := evm.Journal.LoadAccount(beneficiary)
if err != nil {
return
}
beneficiaryResult.Data.Info.Balance.Add(&beneficiaryResult.Data.Info.Balance, &reward)
_ = evm.Journal.BalanceIncr(beneficiary, reward)
}

// applyEIP7702AuthList processes the authorization list for EIP-7702 transactions.
Expand Down Expand Up @@ -751,8 +799,11 @@ func (evm *Evm) applyEIP7702AuthList(tx *Transaction) int64 {
acc := loadResult.Data

// 5. Authority code must be empty or already an EIP-7702 delegation
if len(acc.Info.Code) > 0 && !IsEIP7702Bytecode(acc.Info.Code) {
continue
if acc.Info.CodeHash != types.KeccakEmpty && !acc.Info.CodeHash.IsZero() {
code, err := evm.host.loadCode(authority, acc)
if err != nil || (len(code) > 0 && !IsEIP7702Bytecode(code)) {
continue
}
}

// 6. Nonce must match
Expand Down Expand Up @@ -785,9 +836,33 @@ func (evm *Evm) applyEIP7702AuthList(tx *Transaction) int64 {
return refund
}

var (
secp256k1N = *new(uint256.Int).SetBytes([]byte{
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b,
0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41,
})
secp256k1HalfN = *new(uint256.Int).Rsh(&secp256k1N, 1)
)

func validEIP7702Signature(auth *Authorization) bool {
var r, s uint256.Int
r.SetBytes32(auth.R[:])
s.SetBytes32(auth.S[:])
if auth.YParity > 1 || r.IsZero() || s.IsZero() {
return false
}
return r.Lt(&secp256k1N) && s.Lt(&secp256k1N) && !s.Gt(&secp256k1HalfN)
}

// recoverEIP7702Authority recovers the authority address from an EIP-7702 authorization.
// Signing hash: keccak256(0x05 || rlp([chain_id, address, nonce]))
func recoverEIP7702Authority(auth *Authorization) (types.Address, bool) {
if !validEIP7702Signature(auth) {
return types.Address{}, false
}

// RLP encode [chain_id, address, nonce]
chainIdBytes := rlpEncodeU256Compact(auth.ChainId)
addrBytes := rlpEncodeFixedBytes(auth.Address[:])
Expand Down
44 changes: 44 additions & 0 deletions host/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,50 @@ func TestCalcIntrinsicGasPreIstanbul(t *testing.T) {
}
}

func TestCreate2RecreateAfterPreviousTxSelfdestructAndValueTouch(t *testing.T) {
db := newMockDB()
caller := addr(0x01)
factory := addr(0x20)
initCode := types.Bytes{
0x61, 0x00, 0x10, 0x60, 0x00, 0x81, 0x60, 0x0b, 0x82, 0x39, 0xf3,
0x34, 0x60, 0x00, 0x14, 0x60, 0x0c, 0x57, 0x34, 0x60, 0x00, 0x55, 0x00, 0x5b, 0x60, 0x00, 0xff,
}
runtimeCode := types.Bytes{0x34, 0x60, 0x00, 0x14, 0x60, 0x0c, 0x57, 0x34, 0x60, 0x00, 0x55, 0x00, 0x5b, 0x60, 0x00, 0xff}
factoryCode := types.Bytes{0x36, 0x60, 0x00, 0x60, 0x00, 0x37, 0x60, 0x00, 0x36, 0x60, 0x00, 0x60, 0x00, 0xf5}
child := types.Create2Address(factory, [32]byte{}, types.Keccak256(initCode))

db.accounts[caller] = &state.AccountInfo{Balance: u(1_000_000_000), CodeHash: types.KeccakEmpty}
db.accounts[factory] = &state.AccountInfo{Nonce: 1, Code: factoryCode}
evm := makeEvm(db, spec.Berlin, BlockEnv{GasLimit: u(1_000_000_000)})

for i, tx := range []Transaction{
{Kind: TxKindCall, Caller: caller, To: factory, Input: initCode, GasLimit: 100_000_000, GasPrice: u(1), Nonce: 0},
{Kind: TxKindCall, Caller: caller, To: child, GasLimit: 100_000_000, GasPrice: u(1), Nonce: 1},
{Kind: TxKindCall, Caller: caller, To: child, Value: u(1), GasLimit: 100_000_000, GasPrice: u(1), Nonce: 2},
{Kind: TxKindCall, Caller: caller, To: factory, Input: initCode, GasLimit: 100_000_000, GasPrice: u(1), Nonce: 3},
} {
result := evm.Transact(&tx)
evm.Journal.CommitTx()
if result.Kind != ResultSuccess {
t.Fatalf("tx %d failed: kind=%d reason=%v gas=%d", i, result.Kind, result.Reason, result.GasUsed)
}
}

childAcc := evm.Journal.State[child]
if childAcc == nil {
t.Fatal("child account missing")
}
if childAcc.Info.Nonce != 1 {
t.Fatalf("child nonce: got %d, want 1", childAcc.Info.Nonce)
}
if childAcc.Info.CodeHash != types.Keccak256(runtimeCode) {
t.Fatalf("child code hash: got %x, want recreated runtime hash", childAcc.Info.CodeHash)
}
if childAcc.Info.Balance != u(1) {
t.Fatalf("child balance: got %v, want 1", &childAcc.Info.Balance)
}
}

// --- Transact validation tests ---

func TestTransactIntrinsicGasExceeded(t *testing.T) {
Expand Down
Loading
Loading