Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using Nethermind.Blockchain;
using Nethermind.Core;
using Nethermind.Core.Extensions;
using Nethermind.Core.Specs;
using Nethermind.Core.Test;
using Nethermind.Core.Test.Builders;
using Nethermind.Crypto;
using Nethermind.Evm.State;
using Nethermind.Evm.Tracing;
using Nethermind.Evm.TransactionProcessing;
using Nethermind.Int256;
using Nethermind.Logging;
using Nethermind.Specs;
using Nethermind.Specs.Forks;
using NUnit.Framework;

namespace Nethermind.Evm.Test;

public class TransactionProcessorWarmupTests
{
private ISpecProvider _specProvider = null!;
private IEthereumEcdsa _ethereumEcdsa = null!;
private ITransactionProcessor _transactionProcessor = null!;
private IWorldState _stateProvider = null!;
private IDisposable _worldStateCloser = null!;

[SetUp]
public void Setup()
{
_specProvider = new TestSpecProvider(Prague.Instance);
_stateProvider = TestWorldStateFactory.CreateForTest();
_worldStateCloser = _stateProvider.BeginScope(IWorldState.PreGenesis);
EthereumCodeInfoRepository codeInfoRepository = new(_stateProvider);
EthereumVirtualMachine virtualMachine = new(new TestBlockhashProvider(_specProvider), _specProvider, LimboLogs.Instance);
_transactionProcessor = new EthereumTransactionProcessor(BlobBaseFeeCalculator.Instance, _specProvider, _stateProvider, virtualMachine, codeInfoRepository, LimboLogs.Instance);
_ethereumEcdsa = new EthereumEcdsa(_specProvider.ChainId);
}

[TearDown]
public void TearDown() => _worldStateCloser?.Dispose();

// Warmup must take the real execution path: no-op fee/nonce handling made same-sender
// warm sequences run with undebited balances and unbumped nonces, so deploy chains
// computed wrong CREATE addresses and warmed the wrong state.
[Test]
public void Warmup_InTheThrowawayScope_DebitsFeesAndBumpsTheNonce()
{
_stateProvider.CreateAccount(TestItem.AddressA, 1.Ether);
_stateProvider.Commit(_specProvider.GenesisSpec);
_stateProvider.CommitTree(0);

Transaction tx = Build.A.Transaction
.WithGasPrice(1)
.WithMaxFeePerGas(1)
.WithTo(TestItem.AddressB)
.WithValue(100.GWei)
.WithGasLimit(100_000)
.SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA)
.TestObject;
Block block = Build.A.Block
.WithNumber(long.MaxValue)
.WithTimestamp(MainnetSpecProvider.PragueBlockTimestamp)
.WithTransactions(tx)
.WithGasLimit(10_000_000)
.TestObject;
UInt256 balanceBefore = _stateProvider.GetBalance(TestItem.AddressA);

_transactionProcessor.SetBlockExecutionContext(new BlockExecutionContext(block.Header, _specProvider.GetSpec(block.Header)));
TransactionResult result = _transactionProcessor.Warmup(tx, NullTxTracer.Instance);

Assert.That(result.TransactionExecuted, Is.True, "precondition: the warm execution ran");
Assert.That(_stateProvider.GetNonce(TestItem.AddressA), Is.EqualTo(1UL),
"a warm execution must bump the nonce so a same-sender successor warms the right state");
Assert.That(_stateProvider.GetBalance(TestItem.AddressA), Is.EqualTo(balanceBefore - 100.GWei - 21_000),
"a warm execution must debit value and gas so successors see real balances");
Assert.That(tx.BlockGasUsed, Is.EqualTo(100_000UL),
"warmup must never mutate the shared transaction object: the getter must still fall back to the gas limit");
}
Comment thread
svlachakis marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ public TransactionResult Trace(Transaction transaction, ITxTracer txTracer)
=> transactionProcessor.Process(transaction, txTracer, ExecutionOptions.SkipValidationAndCommit);

/// <summary>
/// Call transaction, no validations, don't commit state.
/// Will NOT charge gas from sender account.
/// Call transaction with real fee and nonce semantics but no validations, don't commit
/// state. Runs in a throwaway scope: charging gas and bumping nonces there keeps
/// same-sender sequences warming the state the real execution will touch.
Comment thread
svlachakis marked this conversation as resolved.
Outdated
/// </summary>
public TransactionResult Warmup(Transaction transaction, ITxTracer txTracer)
=> transactionProcessor.Process(transaction, txTracer, ExecutionOptions.Warmup | ExecutionOptions.SkipValidation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ protected virtual SystemTransactionProcessor<TGasPolicy> CreateSystemTransaction
private TransactionResult ExecuteCore(Transaction tx, ITxTracer tracer, ExecutionOptions opts)
{
if (Logger.IsTrace) Logger.Trace($"Executing tx {tx.Hash}");
if (tx.IsSystem() || (opts & ~ExecutionOptions.Warmup) == ExecutionOptions.SkipValidation)
// Warmup must take the real execution path: the system processor's no-op fee/nonce
Comment thread
svlachakis marked this conversation as resolved.
Outdated
// handling made same-sender warm sequences run with undebited balances and unbumped
// nonces, so deploy chains computed wrong CREATE addresses and warmed the wrong state.
if (tx.IsSystem() || opts == ExecutionOptions.SkipValidation)
{
return GetOrCreateSystemTransactionProcessor().Execute(tx, tracer, opts);
}
Expand All @@ -179,7 +182,7 @@ private TransactionResult ExecuteCore(Transaction tx, ITxTracer tracer, Executio
protected TransactionResult ExecuteCore(Transaction tx, ITxTracer tracer, ExecutionOptions opts, in IntrinsicGas<TGasPolicy> intrinsicGas)
{
if (Logger.IsTrace) Logger.Trace($"Executing tx {tx.Hash}");
if (tx.IsSystem() || (opts & ~ExecutionOptions.Warmup) == ExecutionOptions.SkipValidation)
if (tx.IsSystem() || opts == ExecutionOptions.SkipValidation)
{
return GetOrCreateSystemTransactionProcessor().Execute(tx, tracer, opts);
}
Expand Down
Loading