Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static IEnumerable<BlockchainTest> LoadTests() =>
{
ArchiveVersion = Constants.BalArchiveVersion,
ArchiveName = Constants.BalArchiveName
}, "fixtures/blockchain_tests", typeof(TSelf).GetCustomAttribute<EipWildcardAttribute>()!.Wildcard).LoadTests<BlockchainTest>();
}, "fixtures/blockchain_tests/for_amsterdam", typeof(TSelf).GetCustomAttribute<EipWildcardAttribute>()!.Wildcard).LoadTests<BlockchainTest>();
}

/// <summary>
Expand All @@ -45,5 +45,5 @@ public static IEnumerable<GeneralStateTest> LoadTests() =>
{
ArchiveVersion = Constants.BalArchiveVersion,
ArchiveName = Constants.BalArchiveName
}, "fixtures/state_tests", typeof(TSelf).GetCustomAttribute<EipWildcardAttribute>()!.Wildcard).LoadTests<GeneralStateTest>();
}, "fixtures/state_tests/for_amsterdam", typeof(TSelf).GetCustomAttribute<EipWildcardAttribute>()!.Wildcard).LoadTests<GeneralStateTest>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ namespace Ethereum.Blockchain.Pyspec.Test.Amsterdam;

public static class Constants
{
public const string BalArchiveVersion = "bal@v5.4.0";
public const string BalArchiveVersion = "bal@v5.5.1";
public const string BalArchiveName = "fixtures_bal.tar.gz";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ namespace Ethereum.Blockchain.Pyspec.Test;
public class Constants
{
public const string ARCHIVE_URL_TEMPLATE = "https://github.com/ethereum/execution-spec-tests/releases/download/{0}/{1}";
public const string DEFAULT_ARCHIVE_VERSION = "v5.0.0";
public const string DEFAULT_ARCHIVE_VERSION = "v5.4.0";
public const string DEFAULT_ARCHIVE_NAME = "fixtures_develop.tar.gz";
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,41 @@ public IEnumerable<EthereumTest> Load(string testsDir, string wildcard = null)
}

IEnumerable<string> testDirs = !string.IsNullOrEmpty(testsDir)
? Directory.EnumerateDirectories(Path.Combine(testsDirectoryName, testsDir), "*", new EnumerationOptions { RecurseSubdirectories = true })
? Directory.EnumerateDirectories(ResolveTestsDirectory(testsDirectoryName, testsDir), "*", new EnumerationOptions { RecurseSubdirectories = true })
: Directory.EnumerateDirectories(testsDirectoryName, "*", new EnumerationOptions { RecurseSubdirectories = true });
return testDirs.SelectMany(td => TestLoadStrategy.LoadTestsFromDirectory(td, wildcard, testType));
}

private static string ResolveTestsDirectory(string testsDirectoryName, string testsDir)
{
string requestedDirectory = Path.Combine(testsDirectoryName, testsDir);
if (Directory.Exists(requestedDirectory))
{
return requestedDirectory;
}

string[] parts = testsDir.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries);
bool hadForkPrefix = false;
for (int i = 0; i < parts.Length; i++)
{
if (!parts[i].StartsWith("for_", StringComparison.Ordinal))
{
continue;
}

parts[i] = parts[i]["for_".Length..];
hadForkPrefix = true;
}

if (hadForkPrefix)
{
string legacyDirectory = Path.Combine(testsDirectoryName, Path.Combine(parts));
if (Directory.Exists(legacyDirectory))
{
return legacyDirectory;
}
}

return requestedDirectory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public abstract class PyspecBlockchainTestFixture<TSelf> : BlockchainTestBase

public static IEnumerable<BlockchainTest> LoadTests() =>
new TestsSourceLoader(new LoadPyspecTestsStrategy(),
$"fixtures/blockchain_tests/{TestDirectoryHelper.GetDirectoryByConvention<TSelf>("BlockchainTests")}").LoadTests<BlockchainTest>();
$"fixtures/blockchain_tests/for_{TestDirectoryHelper.GetDirectoryByConvention<TSelf>("BlockchainTests")}").LoadTests<BlockchainTest>();
}

/// <summary>
Expand All @@ -38,5 +38,5 @@ public abstract class PyspecStateTestFixture<TSelf> : GeneralStateTestBase

public static IEnumerable<GeneralStateTest> LoadTests() =>
new TestsSourceLoader(new LoadPyspecTestsStrategy(),
$"fixtures/state_tests/{TestDirectoryHelper.GetDirectoryByConvention<TSelf>("StateTests")}").LoadTests<GeneralStateTest>();
$"fixtures/state_tests/for_{TestDirectoryHelper.GetDirectoryByConvention<TSelf>("StateTests")}").LoadTests<GeneralStateTest>();
}
5 changes: 4 additions & 1 deletion src/Nethermind/Ethereum.Test.Base/GeneralTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ protected EthereumTestResult RunTest(GeneralStateTest test, ITxTracer txTracer)
stateProvider.RecalculateStateRoot();
}

Snapshot preExecutionSnapshot = stateProvider.TakeSnapshot(newTransactionStart: true);

if (test.Transaction.ChainId is null)
{
test.Transaction.ChainId = test.ChainId;
Expand Down Expand Up @@ -180,7 +182,8 @@ protected EthereumTestResult RunTest(GeneralStateTest test, ITxTracer txTracer)
}
else
{
stateProvider.Reset();
stateProvider.Restore(preExecutionSnapshot);
stateProvider.RecalculateStateRoot();
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,21 @@ public static Transaction Convert(PostStateJson postStateJson, TransactionJson t
};
transaction.Hash = transaction.CalculateHash();

bool hasAccessListField = transactionJson.AccessLists is not null || transactionJson.AccessList is not null;
AccessList.Builder builder = new();
ProcessAccessList(transactionJson.AccessLists is not null
? transactionJson.AccessLists[postStateJson.Indexes.Data]
: transactionJson.AccessList, builder);
transaction.AccessList = builder.Build();

if (transaction.AccessList.AsEnumerable().Count() != 0)
if (hasAccessListField)
{
transaction.Type = TxType.AccessList;
else
}
else if (transaction.AccessList.IsEmpty)
{
transaction.AccessList = null;
}

if (transactionJson.MaxFeePerGas is not null)
{
Expand Down
85 changes: 85 additions & 0 deletions src/Nethermind/Ethereum.Transaction.Test/TransactionJsonTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
using Ethereum.Test.Base;
using FluentAssertions;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Eip2930;
using Nethermind.Core.Test.Builders;
using Nethermind.Int256;
using Nethermind.Serialization.Json;
using Nethermind.Specs.Forks;
using NUnit.Framework;

namespace Ethereum.Blockchain.Test;
Expand Down Expand Up @@ -35,4 +38,86 @@ public void Can_load_access_lists()
Nethermind.Core.Transaction tx = JsonToEthereumTest.Convert(new PostStateJson { Indexes = new IndexesJson() }, txJson);
tx.AccessList.Should().NotBeNull();
}

[Test]
public void Convert_sets_AccessList_type_when_accessLists_field_present_but_empty()
{
const string json =
"""{"accessLists": [[]], "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": ["0x00"], "gasLimit": ["0x0186a0"], "data": ["0x"]}""";

EthereumJsonSerializer serializer = new();
TransactionJson txJson = serializer.Deserialize<TransactionJson>(json);

Nethermind.Core.Transaction tx = JsonToEthereumTest.Convert(new PostStateJson { Indexes = new IndexesJson() }, txJson);

tx.Type.Should().Be(TxType.AccessList,
"presence of accessLists field (even empty) should set Type 1");
}

/// <summary>
/// An AccessList transaction with an empty access list sent against Istanbul (pre-Berlin)
/// must be rejected. The post-state root must equal the pre-state root - the invalid tx
/// should not mutate state.
/// Expected hash from pyspec: test_eip2930_tx_validity[fork_Istanbul-invalid-state_test]
/// </summary>
[Test]
public void Invalid_pre_berlin_access_list_tx_with_empty_list_preserves_prestate_root()
{
Address sender = new("0x1ad9bc24818784172ff393bb6f89f094d4d2ca29");
Address recipient = new("0x67eb8fcbef83a0662b030f8bc89a10070c167a66");

Nethermind.Core.Transaction transaction = Build.A.Transaction
.WithType(TxType.AccessList)
.WithChainId(1)
.WithAccessList(AccessList.Empty)
.WithGasLimit(100_000)
.WithGasPrice(10)
.WithNonce(UInt256.Zero)
.To(recipient)
.WithValue(0)
.SignedAndResolved(TestItem.PrivateKeyA)
.TestObject;
// Override sender to match the pyspec fixture key
transaction.SenderAddress = sender;

GeneralStateTest test = new()
{
Name = nameof(Invalid_pre_berlin_access_list_tx_with_empty_list_preserves_prestate_root),
Category = "state",
Fork = Istanbul.Instance,
ForkName = Istanbul.Instance.Name,
CurrentCoinbase = new Address("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba"),
CurrentDifficulty = new UInt256(0x020000),
CurrentGasLimit = 120_000_000,
CurrentNumber = 1,
CurrentTimestamp = 1000,
PreviousHash = Keccak.Zero,
Pre = new()
{
[recipient] = new()
{
Nonce = UInt256.One,
Balance = UInt256.Zero,
Code = [0x60, 0x01, 0x60, 0x00, 0x55], // PUSH1 1 PUSH1 0 SSTORE
Storage = new() { [UInt256.Zero] = new UInt256(0xdeadbeef).ToBigEndian() }
},
[sender] = new()
{
Nonce = UInt256.Zero,
Balance = UInt256.Parse("1000000000000000000000"),
Code = [],
Storage = new()
}
},
// Expected post-state root from pyspec fixture (pre-state unchanged)
PostHash = new Hash256("0x43c19943b2c4a638fe07dbc954c1422032ea7c5e17d0d659f25a5324ed75f0be"),
Transaction = transaction,
};

EthereumTestResult result = RunTest(test);

result.StateRoot.Should().Be(test.PostHash,
"invalid AccessList tx on pre-Berlin fork should not mutate state");
result.Pass.Should().BeTrue();
}
}
35 changes: 35 additions & 0 deletions src/Nethermind/Nethermind.Evm.Test/Eip8037RegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using Nethermind.Core.Extensions;
using Nethermind.Core.Specs;
using Nethermind.Core.Test.Builders;
using Nethermind.Evm.State;
using Nethermind.Evm.Tracing;
using Nethermind.Int256;
using Nethermind.Specs;
Expand Down Expand Up @@ -76,4 +78,37 @@ public void Eip8037_nested_create_code_deposit_must_not_borrow_parent_regular_ga
Assert.That(returnData.IsZero(), Is.True,
"Nested CREATE should fail: child has 1175 gas but needs 1180 for code deposit (6 regular + 1174 state spill)");
}

/// <summary>
/// A child CALL that runs out of gas during SSTORE must not spill state gas into the
/// parent frame's reservoir. If it does, the parent can incorrectly complete its own
/// SSTORE with gas that should have been burned by the child halt.
/// </summary>
[Test]
public void Eip8037_failed_child_sstore_must_not_inflate_parent_state_reservoir()
{
byte[] childCode = Prepare.EvmCode
.PushData(1)
.PushData(0)
.Op(Instruction.SSTORE)
.Op(Instruction.STOP)
.Done;

TestState.CreateAccount(TestItem.AddressC, 1.Ether);
TestState.InsertCode(TestItem.AddressC, childCode, SpecProvider.GenesisSpec);

byte[] parentCode = Prepare.EvmCode
.Call(TestItem.AddressC, 40_000)
.PushData(1)
.PushData(0)
.Op(Instruction.SSTORE)
.Op(Instruction.STOP)
.Done;

TestAllTracerWithOutput tracer = Execute(Activation, 70_000, parentCode);

Assert.That(tracer.StatusCode, Is.EqualTo(StatusCode.Failure),
"The parent SSTORE should run out of gas once the child CALL burns its own failed SSTORE gas.");
Assert.That(tracer.Error, Is.EqualTo("OutOfGas"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,9 @@ internal static EvmExceptionType InstructionSStoreMetered<TGasPolicy, TTracingIn
{
bool ssetOutOfGas = TEip8037.IsActive switch
{
true => !TGasPolicy.ConsumeStateGas(ref gas, GasCostOf.SSetState) || !TGasPolicy.UpdateGas(ref gas, GasCostOf.SSetRegular),
// EIP-8037: charge the regular component first so an OOG halt does not
// spill state gas into gas_left and then restore it to the parent frame.
true => !TGasPolicy.TryConsumeStateAndRegularGas(ref gas, GasCostOf.SSetState, GasCostOf.SSetRegular),
false => !TGasPolicy.UpdateGas(ref gas, GasCostOf.SSet),
};

Expand Down
Loading