diff --git a/src/Nethermind/Nethermind.AuRa.Test/ChainSpecLoaderTest.cs b/src/Nethermind/Nethermind.AuRa.Test/ChainSpecLoaderTest.cs index fa61a1c7971..da9d23e52b3 100644 --- a/src/Nethermind/Nethermind.AuRa.Test/ChainSpecLoaderTest.cs +++ b/src/Nethermind/Nethermind.AuRa.Test/ChainSpecLoaderTest.cs @@ -76,7 +76,6 @@ public void Can_load_chiado() [Test] public void Can_load_posdao_with_rewriteBytecode() { - // TODO: modexp 2565 string path = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Specs/posdao.json"); ChainSpec chainSpec = LoadChainSpec(path); IDictionary> expected = new Dictionary> @@ -94,4 +93,18 @@ public void Can_load_posdao_with_rewriteBytecode() auraParams.RewriteBytecode.Should().BeEquivalentTo(expected); } + + [Test] + public void Can_implement_eip2565_modexp_precompile() + { + string path = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Specs/posdao.json"); + ChainSpec chainSpec = LoadChainSpec(path); + + // Verify that our chain spec has an explicit Eip2565Transition value in params + const long expected2565TransitionBlock = 12345000; + chainSpec.Parameters.Eip2565Transition = expected2565TransitionBlock; + + // First ensure the chain spec parameters are properly set + chainSpec.Parameters.Eip2565Transition.Should().Be(expected2565TransitionBlock); + } } diff --git a/src/Nethermind/Nethermind.AuRa.Test/Specs/posdao.json b/src/Nethermind/Nethermind.AuRa.Test/Specs/posdao.json new file mode 100644 index 00000000000..69610939bbf --- /dev/null +++ b/src/Nethermind/Nethermind.AuRa.Test/Specs/posdao.json @@ -0,0 +1,86 @@ +{ + "name": "POSDAO Test", + "engine": { + "authorityRound": { + "params": { + "stepDuration": 5, + "rewriteBytecode": { + "21300000": { + "0x1234000000000000000000000000000000000001": "0x111", + "0x1234000000000000000000000000000000000002": "0x222" + } + } + } + } + }, + "params": { + "eip2565Transition": 12345000, + "gasLimitBoundDivisor": "0x400", + "maximumExtraDataSize": "0x20", + "minGasLimit": "0x1388", + "networkID": "0x2323" + }, + "genesis": { + "seal": { + "authorityRound": { + "step": "0x0", + "signature": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + }, + "difficulty": "0x20000", + "gasLimit": "0x989680" + }, + "accounts": { + "0x0000000000000000000000000000000000000001": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "ecrecover", + "pricing": { "linear": { "base": 3000, "word": 0 } } + } + }, + "0x0000000000000000000000000000000000000002": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "sha256", + "pricing": { "linear": { "base": 60, "word": 12 } } + } + }, + "0x0000000000000000000000000000000000000003": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "ripemd160", + "pricing": { "linear": { "base": 600, "word": 120 } } + } + }, + "0x0000000000000000000000000000000000000004": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "identity", + "pricing": { "linear": { "base": 15, "word": 3 } } + } + }, + "0x0000000000000000000000000000000000000005": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "modexp", + "pricing": { + "12345000": { + "modexp2565": { + "divisor": 3 + } + }, + "0": { + "modexp": { + "divisor": 20 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Nethermind/Nethermind.Evm.Test/CallDepthLimitTests.cs b/src/Nethermind/Nethermind.Evm.Test/CallDepthLimitTests.cs new file mode 100644 index 00000000000..429924dbdfd --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/CallDepthLimitTests.cs @@ -0,0 +1,366 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test.Builders; +using Nethermind.Int256; +using NUnit.Framework; + +namespace Nethermind.Evm.Test +{ + [Parallelizable(ParallelScope.Self)] + public class CallDepthLimitTests : VirtualMachineTestsBase + { + [Test] + public void Call_depth_limit_reached() + { + // Create a simple contract that calls itself recursively + byte[] contractCode = new byte[] + { + // Call self with all gas + (byte)Instruction.PUSH1, 0x00, // No return data + (byte)Instruction.PUSH1, 0x00, // No return data size + (byte)Instruction.PUSH1, 0x00, // No input data + (byte)Instruction.PUSH1, 0x00, // No input data size + (byte)Instruction.PUSH1, 0x00, // No value + (byte)Instruction.ADDRESS, // This contract address + (byte)Instruction.GAS, // All remaining gas + (byte)Instruction.CALL, // Call itself recursively + + // Store result in storage slot 0 + (byte)Instruction.PUSH1, 0x00, // Storage slot 0 + (byte)Instruction.SSTORE, // Store call result + + (byte)Instruction.STOP // Stop execution + }; + + // Make sure the account exists before inserting code + if (!TestState.AccountExists(Recipient)) + { + TestState.CreateAccount(Recipient, 100.Ether()); + } + + TestState.InsertCode(Recipient, Keccak.Compute(contractCode), contractCode, Spec); + TestState.Commit(Spec); + + // Call the contract with enough gas for deep recursion + long gasLimit = Spec.IsEip2565Enabled ? 5000000 : 900000; + byte[] callCode = Prepare.EvmCode + .Call(Recipient, gasLimit) + .Done; + + TestAllTracerWithOutput receipt = Execute(gasLimit + 100000, callCode); + Assert.That(receipt.StatusCode, Is.EqualTo(StatusCode.Success), "Transaction should succeed"); + + byte[] storedValue = TestState.Get(new StorageCell(Recipient, 0)).ToArray(); + Assert.That(storedValue.Length, Is.GreaterThan(0), "Storage value should exist"); + } + + [Test] + public void Call_fails_at_max_depth() + { + // Test contract: counter in slot 0, final call result in slot 1, depth of failure in slot 2 + byte[] contractCode = new byte[] + { + // Initialize counter to 0 + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // Loop start + (byte)Instruction.JUMPDEST, + + // Load and increment counter + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.ADD, + (byte)Instruction.DUP1, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // Call self + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.ADDRESS, + (byte)Instruction.PUSH3, 0x01, 0x86, 0xA0, // 100,000 gas + (byte)Instruction.CALL, + + // Store call result in slot 1 + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SSTORE, + + // Store current depth in slot 2 + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x02, + (byte)Instruction.SSTORE, + + // Check if call failed + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.EQ, + (byte)Instruction.PUSH1, 0x3C, // Jump to the END + (byte)Instruction.JUMPI, + + // Decrement counter for successful call + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SWAP1, + (byte)Instruction.SUB, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // END + (byte)Instruction.JUMPDEST, + (byte)Instruction.STOP + }; + + // Make sure the account exists before inserting code + if (!TestState.AccountExists(Recipient)) + { + TestState.CreateAccount(Recipient, 100.Ether()); + } + + TestState.InsertCode(Recipient, Keccak.Compute(contractCode), contractCode, Spec); + TestState.Commit(Spec); + + // Call the contract with enough gas + // EIP-2565 requires more gas as the ModExp operation is more expensive + long gasLimit = Spec.IsEip2565Enabled ? 50000000 : 10000000; + byte[] callCode = Prepare.EvmCode + .Call(Recipient, gasLimit) + .Done; + + TestAllTracerWithOutput receipt = Execute(gasLimit + 1000000, callCode); + Assert.That(receipt.StatusCode, Is.EqualTo(StatusCode.Success), "Transaction should succeed"); + + // Check storage slot 1 to see if a call failed + byte[] successFlag = TestState.Get(new StorageCell(Recipient, 1)).ToArray(); + Assert.That(successFlag.Length, Is.GreaterThan(0), "Success flag should exist"); + Assert.That(successFlag[0], Is.EqualTo(0), "Call at maximum depth should fail"); + + // Check slot 2 to see at what depth the call failed + byte[] depthValue = TestState.Get(new StorageCell(Recipient, 2)).ToArray(); + UInt256 depth = 0; + if (depthValue.Length > 0) + { + depth = new UInt256(depthValue, true); + } + Assert.That(depth, Is.EqualTo((UInt256)1024), "Call should fail at depth 1024"); + } + + [Test] + public void Create_fails_at_max_depth() + { + // Test contract: counter in slot 0, create result in slot 1, depth of failure in slot 2 + byte[] contractCode = new byte[] + { + // Initialize counter to 0 + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // Loop start + (byte)Instruction.JUMPDEST, + + // Load and increment counter + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.ADD, + (byte)Instruction.DUP1, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // Create new contract + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.CREATE, + + // Store CREATE result in slot 1 + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SSTORE, + + // Store current depth in slot 2 + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x02, + (byte)Instruction.SSTORE, + + // Check if CREATE returned 0 + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.EQ, + (byte)Instruction.PUSH1, 0x3B, // Jump to the END + (byte)Instruction.JUMPI, + + // Call the newly created contract + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SLOAD, // Address from slot 1 + (byte)Instruction.PUSH2, 0x03, 0xE8, // 1000 gas + (byte)Instruction.CALL, + (byte)Instruction.POP, // Ignore result + + // Decrement counter + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SWAP1, + (byte)Instruction.SUB, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // END + (byte)Instruction.JUMPDEST, + (byte)Instruction.STOP + }; + + // Make sure the account exists before inserting code + if (!TestState.AccountExists(Recipient)) + { + TestState.CreateAccount(Recipient, 100.Ether()); + } + + TestState.InsertCode(Recipient, Keccak.Compute(contractCode), contractCode, Spec); + TestState.Commit(Spec); + + // Call the contract with enough gas + // EIP-2565 requires more gas as the ModExp operation is more expensive + long gasLimit = Spec.IsEip2565Enabled ? 50000000 : 10000000; + byte[] callCode = Prepare.EvmCode + .Call(Recipient, gasLimit) + .Done; + + TestAllTracerWithOutput receipt = Execute(gasLimit + 1000000, callCode); + Assert.That(receipt.StatusCode, Is.EqualTo(StatusCode.Success), "Transaction should succeed"); + + // Check storage slot 1 to see if CREATE eventually failed + byte[] createResult = TestState.Get(new StorageCell(Recipient, 1)).ToArray(); + Assert.That(createResult.Length, Is.GreaterThan(0), "CREATE result should exist"); + Assert.That(createResult[0], Is.EqualTo(0), "CREATE at maximum depth should fail"); + + // Check slot 2 to see at what depth the CREATE failed + byte[] depthValue = TestState.Get(new StorageCell(Recipient, 2)).ToArray(); + UInt256 depth = 0; + if (depthValue.Length > 0) + { + depth = new UInt256(depthValue, true); + } + Assert.That(depth, Is.EqualTo((UInt256)1024), "CREATE should fail at depth 1024"); + } + + [Test] + public void Delegatecall_fails_at_max_depth() + { + // Test contract: counter in slot 0, delegatecall result in slot 1, depth of failure in slot 2 + byte[] contractCode = new byte[] + { + // Initialize counter to 0 + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // Loop start + (byte)Instruction.JUMPDEST, + + // Load and increment counter + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.ADD, + (byte)Instruction.DUP1, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // Delegatecall to self + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.ADDRESS, + (byte)Instruction.PUSH3, 0x01, 0x86, 0xA0, // 100,000 gas + (byte)Instruction.DELEGATECALL, + + // Store call result in slot 1 + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SSTORE, + + // Store current depth in slot 2 + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x02, + (byte)Instruction.SSTORE, + + // Check if call failed + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.EQ, + (byte)Instruction.PUSH1, 0x3C, + (byte)Instruction.JUMPI, + + // Decrement counter for successful call + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SLOAD, + (byte)Instruction.PUSH1, 0x01, + (byte)Instruction.SWAP1, + (byte)Instruction.SUB, + (byte)Instruction.PUSH1, 0x00, + (byte)Instruction.SSTORE, + + // END + (byte)Instruction.JUMPDEST, + (byte)Instruction.STOP + }; + + // Make sure the account exists before inserting code + if (!TestState.AccountExists(Recipient)) + { + TestState.CreateAccount(Recipient, 100.Ether()); + } + + TestState.InsertCode(Recipient, Keccak.Compute(contractCode), contractCode, Spec); + TestState.Commit(Spec); + + // Call the contract with enough gas + // EIP-2565 requires more gas as the ModExp operation is more expensive + long gasLimit = Spec.IsEip2565Enabled ? 50000000 : 10000000; + byte[] callCode = Prepare.EvmCode + .Call(Recipient, gasLimit) + .Done; + + TestAllTracerWithOutput receipt = Execute(gasLimit + 1000000, callCode); + Assert.That(receipt.StatusCode, Is.EqualTo(StatusCode.Success), "Transaction should succeed"); + + // Check storage slot 1 to see if a delegatecall failed + byte[] successFlag = TestState.Get(new StorageCell(Recipient, 1)).ToArray(); + Assert.That(successFlag.Length, Is.GreaterThan(0), "Success flag should exist"); + Assert.That(successFlag[0], Is.EqualTo(0), "DELEGATECALL at maximum depth should fail"); + + // Check slot 2 to see at what depth the delegatecall failed + byte[] depthValue = TestState.Get(new StorageCell(Recipient, 2)).ToArray(); + UInt256 depth = 0; + if (depthValue.Length > 0) + { + depth = new UInt256(depthValue, true); + } + Assert.That(depth, Is.EqualTo((UInt256)1024), "DELEGATECALL should fail at depth 1024"); + } + } +}