diff --git a/src/Nethermind/Nethermind.Blockchain.Test/TransactionProcessorTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/TransactionProcessorTests.cs index bb8c1fe456df..b2b89a875891 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/TransactionProcessorTests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/TransactionProcessorTests.cs @@ -466,7 +466,7 @@ public void Can_estimate_with_destroy_refund_and_below_intrinsic_pre_berlin() tracer.CalculateAdditionalGasRequired(tx, releaseSpec).Should().Be(24080); tracer.GasSpent.Should().Be(35228L); long estimate = estimator.Estimate(tx, block.Header, tracer, out string? err, 0); - estimate.Should().Be(59307); + estimate.Should().Be(54225); Assert.That(err, Is.Null); ConfirmEnoughEstimate(tx, block, estimate); @@ -474,32 +474,19 @@ public void Can_estimate_with_destroy_refund_and_below_intrinsic_pre_berlin() private void ConfirmEnoughEstimate(Transaction tx, Block block, long estimate) { - CallOutputTracer outputTracer = new(); - tx.GasLimit = estimate; - TestContext.Out.WriteLine(tx.GasLimit); - - GethLikeTxMemoryTracer gethTracer = new(tx, GethTraceOptions.Default); var blkCtx = new BlockExecutionContext(block.Header, _specProvider.GetSpec(block.Header)); - _transactionProcessor.CallAndRestore(tx, blkCtx, gethTracer); - string traceEnoughGas = new EthereumJsonSerializer().Serialize(gethTracer.BuildResult(), true); + CallOutputTracer outputTracer = new(); + tx.GasLimit = estimate; _transactionProcessor.CallAndRestore(tx, blkCtx, outputTracer); - traceEnoughGas.Should().NotContain("OutOfGas"); + outputTracer.StatusCode.Should().Be(StatusCode.Success, + $"transaction should succeed at the estimate ({estimate})"); outputTracer = new CallOutputTracer(); tx.GasLimit = Math.Min(estimate - 1, estimate * 63 / 64); - TestContext.Out.WriteLine(tx.GasLimit); - - gethTracer = new GethLikeTxMemoryTracer(tx, GethTraceOptions.Default); - _transactionProcessor.CallAndRestore(tx, blkCtx, gethTracer); - - string traceOutOfGas = new EthereumJsonSerializer().Serialize(gethTracer.BuildResult(), true); - TestContext.Out.WriteLine(traceOutOfGas); - _transactionProcessor.CallAndRestore(tx, blkCtx, outputTracer); - - bool failed = traceEnoughGas.Contains("failed") || traceEnoughGas.Contains("OutOfGas"); - failed.Should().BeTrue(); + outputTracer.StatusCode.Should().Be(StatusCode.Failure, + $"transaction should fail below the estimate ({tx.GasLimit})"); } [TestCase] diff --git a/src/Nethermind/Nethermind.Blockchain/Tracing/EstimateGasTracer.cs b/src/Nethermind/Nethermind.Blockchain/Tracing/EstimateGasTracer.cs index 1d6e7187eaaf..1d6f49def139 100644 --- a/src/Nethermind/Nethermind.Blockchain/Tracing/EstimateGasTracer.cs +++ b/src/Nethermind/Nethermind.Blockchain/Tracing/EstimateGasTracer.cs @@ -149,11 +149,14 @@ public void ReportActionError(EvmExceptionType exceptionType, long gasLeft) public override void ReportOperationError(EvmExceptionType error) { - OutOfGas |= error == EvmExceptionType.OutOfGas; - - if (error == EvmExceptionType.Revert && _currentNestingLevel == 0) + if (_currentNestingLevel == 0) { - TopLevelRevert = true; + OutOfGas |= error == EvmExceptionType.OutOfGas; + + if (error == EvmExceptionType.Revert) + { + TopLevelRevert = true; + } } } diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs index 804288dffcf9..16572c02c27b 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs @@ -669,6 +669,598 @@ public void Should_fail_with_top_level_revert() testEnvironment.tracer.TopLevelRevert.Should().BeTrue(); } + [Test] + public void Should_estimate_gas_when_inner_call_reverts_but_transaction_succeeds() + { + // Reproduces https://github.com/NethermindEth/nethermind/issues/10552 + // GnosisSafe createProxyWithNonce has inner calls that revert (try/catch pattern). + // The bug: ReportOperationError sets OutOfGas=true for ANY revert, even inner ones, + // causing the binary search in gas estimation to always think the tx failed. + using TestEnvironment testEnvironment = new(); + + Address reverterAddress = TestItem.AddressB; + Address callerAddress = TestItem.AddressC; + + // Reverter contract: always reverts with empty data + byte[] reverterCode = Prepare.EvmCode + .PushData(0x00) + .PushData(0x00) + .Op(Instruction.REVERT) + .Done; + testEnvironment.InsertContract(reverterAddress, reverterCode); + + // Caller contract: CALLs reverter (which reverts), catches the revert, then succeeds. + // This simulates GnosisSafe's try/catch pattern. + byte[] callerCode = Prepare.EvmCode + .Call(reverterAddress, 100_000) // inner call that reverts - return value 0 on stack + .Op(Instruction.POP) // discard call result + .PushData(0x01) // value = 1 + .PushData(0x00) // key = 0 + .Op(Instruction.SSTORE) // store 1 at slot 0 (proves execution continued) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(callerAddress, callerCode); + + long gasLimit = 300_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(callerAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().BeGreaterThan(0, "Gas estimation should succeed when inner call reverts but transaction succeeds overall"); + err.Should().BeNull("No error should occur - inner reverts should not be treated as top-level failures"); + } + + [Test] + public void Should_estimate_gas_for_create2_with_setup_call_pattern() + { + // Simulates GnosisSafe createProxyWithNonce: CREATE2 deploys a proxy, then + // the caller does a CALL to the newly deployed proxy for setup. + // The setup call may revert internally but the overall tx succeeds. + using TestEnvironment testEnvironment = new(); + + // The "proxy" runtime code: just stores a value (simulating successful setup) + byte[] proxyRuntimeCode = Prepare.EvmCode + .PushData(0x42) // value + .PushData(0x00) // key + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + + // Init code that returns the runtime code + byte[] initCode = Prepare.EvmCode + .ForInitOf(proxyRuntimeCode) + .Done; + + // Factory contract: CREATE2 the proxy, then CALL setup on it + // CREATE2(value=0, offset=0, size=initCode.length, salt=0) + // CALL(gas, addr_from_create2, value=0, ...) + byte[] factoryCode = Prepare.EvmCode + .Create2(initCode, new byte[] { 0x01 }, 0) // CREATE2 with salt=1 + .Op(Instruction.DUP1) // duplicate address for CALL + .PushData(0x00) // retSize + .PushData(0x00) // retOffset + .PushData(0x00) // argSize + .PushData(0x00) // argOffset + .PushData(0x00) // value + .Op(Instruction.SWAP5) // bring address to top (after value) + .PushData(50_000) // gas for setup call + .Op(Instruction.CALL) + .Op(Instruction.POP) // discard call result + .Op(Instruction.POP) // discard remaining address copy + .Op(Instruction.STOP) + .Done; + + Address factoryAddress = TestItem.AddressB; + testEnvironment.InsertContract(factoryAddress, factoryCode); + + long gasLimit = 500_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(factoryAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ConstantinopleFixBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().BeGreaterThan(0, "Gas estimation should succeed for CREATE2 + setup call pattern"); + err.Should().BeNull("No error for CREATE2 + setup call"); + } + + [Test] + public void Should_estimate_gas_with_multiple_inner_calls_mixed_reverts() + { + // Contract makes 3 inner calls: first reverts, second succeeds, third reverts. + // Transaction should still succeed and gas estimation should work. + using TestEnvironment testEnvironment = new(); + + Address reverterAddress = TestItem.AddressB; + Address succeederAddress = TestItem.AddressC; + + // Contract that always reverts + byte[] reverterCode = Prepare.EvmCode + .Revert(0, 0) + .Done; + testEnvironment.InsertContract(reverterAddress, reverterCode); + + // Contract that succeeds (stores value) + byte[] succeederCode = Prepare.EvmCode + .PushData(0x01) + .PushData(0x00) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(succeederAddress, succeederCode); + + // Caller: calls reverter, succeeder, reverter - catches all failures + Address callerAddress = TestItem.AddressD; + byte[] callerCode = Prepare.EvmCode + .Call(reverterAddress, 30_000) // call 1: reverts + .Op(Instruction.POP) + .Call(succeederAddress, 50_000) // call 2: succeeds + .Op(Instruction.POP) + .Call(reverterAddress, 30_000) // call 3: reverts + .Op(Instruction.POP) + .PushData(0xFF) + .PushData(0x01) + .Op(Instruction.SSTORE) // store to prove we got here + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(callerAddress, callerCode); + + long gasLimit = 500_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(callerAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().BeGreaterThan(0, "Gas estimation should succeed with mixed inner reverts"); + err.Should().BeNull("No error when inner calls revert but overall tx succeeds"); + } + + [Test] + public void Should_estimate_gas_when_inner_call_runs_out_of_gas_but_caller_handles_it() + { + // Verifies that inner OOG (caught by the caller) does not fail gas estimation. + // OutOfGas must be nesting-aware (only set at top level), matching Geth behavior. + // Geth's binary search checks result.Failed() which only reflects the top-level outcome. + // See: https://github.com/ethereum/go-ethereum/blob/master/eth/gasestimator/gasestimator.go + using TestEnvironment testEnvironment = new(); + + // Contract that consumes all gas via infinite loop (will always OOG) + Address gasGuzzlerAddress = TestItem.AddressB; + byte[] gasGuzzlerCode = Prepare.EvmCode + .Op(Instruction.JUMPDEST) // offset 0 + .PushData((byte)0x00) + .Op(Instruction.JUMP) // jump back to 0 + .Done; + testEnvironment.InsertContract(gasGuzzlerAddress, gasGuzzlerCode); + + // Middle contract: calls gas guzzler with limited gas, catches OOG + Address middleAddress = TestItem.AddressC; + byte[] middleCode = Prepare.EvmCode + .Call(gasGuzzlerAddress, 1_000) // only 1000 gas - will OOG + .Op(Instruction.POP) // discard result (0 = failure) + .PushData(0x01) + .PushData((byte)0x00) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(middleAddress, middleCode); + + // Outer caller + Address callerAddress = TestItem.AddressD; + byte[] callerCode = Prepare.EvmCode + .Call(middleAddress, 100_000) + .Op(Instruction.POP) + .PushData(0x02) + .PushData(0x01) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(callerAddress, callerCode); + + long gasLimit = 500_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(callerAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().BeGreaterThan(0, "Gas estimation should succeed when inner call OOGs but caller handles it"); + err.Should().BeNull("No error - inner OOG should not affect top-level estimation"); + } + + [TestCase(50_000, true)] + [TestCase(500_000, true)] + [TestCase(1_000, false)] + public void Should_estimate_gas_with_gas_sensitive_branching(long gasThreshold, bool shouldSucceed) + { + // Contract that checks gasLeft() and branches: if gasLeft >= threshold, SSTORE; else REVERT. + // Tests that the binary search correctly handles gas-dependent execution paths. + using TestEnvironment testEnvironment = new(); + + Address contractAddress = TestItem.AddressB; + + // Use the existing pattern from Should_estimate_gas_for_explicit_gas_check_and_revert + // Bytecode: PUSH3 , GAS, LT, PUSH1 , JUMPI, PUSH1 1, PUSH1 0, SSTORE, STOP, JUMPDEST, PUSH1 0, PUSH1 0, REVERT + var check = gasThreshold; + byte[] contractCode = Bytes.FromHexString($"0x62{check:x6}5a10600f576001600055005b6000806000fd"); + testEnvironment.InsertContract(contractAddress, contractCode); + + long gasLimit = 1_100_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(contractAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + if (shouldSucceed) + { + result.Should().BeGreaterThan(0, "Gas estimation should find enough gas for the success path"); + err.Should().BeNull("No error - binary search should find gas level above threshold"); + } + else + { + result.Should().BeGreaterThan(0, "Low threshold should always succeed"); + err.Should().BeNull(); + } + } + + [Test] + public void Should_estimate_gas_for_create_with_constructor_making_calls() + { + // CREATE deploys a contract whose constructor makes an external CALL. + // The constructor call might revert but CREATE still succeeds. + using TestEnvironment testEnvironment = new(); + + // External contract that reverts + Address externalAddress = TestItem.AddressB; + byte[] externalCode = Prepare.EvmCode + .Revert(0, 0) + .Done; + testEnvironment.InsertContract(externalAddress, externalCode); + + // Runtime code (deployed contract's code) + byte[] runtimeCode = Prepare.EvmCode + .PushData(0x01) + .PushData(0x00) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + + // Init code: calls external (which reverts, but init code catches it), then returns runtime code + byte[] initCode = Prepare.EvmCode + .Call(externalAddress, 10_000) + .Op(Instruction.POP) // discard call result + .ForInitOf(runtimeCode) + .Done; + + // Factory: CREATE with init code, then STOP + Address factoryAddress = TestItem.AddressC; + byte[] factoryCode = Prepare.EvmCode + .Create(initCode, 0) + .Op(Instruction.POP) // discard created address + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(factoryAddress, factoryCode); + + long gasLimit = 500_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(factoryAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().BeGreaterThan(0, "Gas estimation should succeed for CREATE with constructor that makes calls"); + err.Should().BeNull("No error for constructor-call pattern"); + } + + [Test] + public void Should_estimate_gas_consistently_across_repeated_calls() + { + // Tests that repeated gas estimation on the same contract yields consistent results. + // Each call creates a fresh EstimateGasTracer; this guards against non-deterministic estimation behavior across runs. + using TestEnvironment testEnvironment = new(); + + Address reverterAddress = TestItem.AddressB; + byte[] reverterCode = Prepare.EvmCode + .Revert(0, 0) + .Done; + testEnvironment.InsertContract(reverterAddress, reverterCode); + + Address callerAddress = TestItem.AddressC; + byte[] callerCode = Prepare.EvmCode + .Call(reverterAddress, 30_000) + .Op(Instruction.POP) + .PushData(0x01) + .PushData(0x00) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(callerAddress, callerCode); + + long gasLimit = 300_000; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithGasLimit(gasLimit) + .TestObject; + + long? firstResult = null; + for (int i = 0; i < 10; i++) + { + // Each estimation uses a fresh tracer (as BlockchainBridge.EstimateGas does) + TestEnvironment freshEnv = new(); + freshEnv.InsertContract(reverterAddress, reverterCode); + freshEnv.InsertContract(callerAddress, callerCode); + + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(callerAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + + long result = freshEnv.estimator.Estimate(tx, block.Header, freshEnv.tracer, out string? err); + + result.Should().BeGreaterThan(0, $"Iteration {i}: gas estimation should succeed"); + err.Should().BeNull($"Iteration {i}: no error expected"); + + firstResult ??= result; + result.Should().Be(firstResult.Value, $"Iteration {i}: result should be consistent"); + + freshEnv.Dispose(); + } + } + + [Test] + public void Should_estimate_gas_for_deeply_nested_calls() + { + // Chain of 4 nested CALLs to test nesting level tracking in EstimateGasTracer. + // A -> B -> C -> D (all succeed) + using TestEnvironment testEnvironment = new(); + + // Contract D: leaf, just stores and stops + Address addrD = TestItem.AddressD; + byte[] codeD = Prepare.EvmCode + .PushData(0x04) + .PushData(0x04) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(addrD, codeD); + + // Contract C: calls D + Address addrC = TestItem.AddressC; + byte[] codeC = Prepare.EvmCode + .Call(addrD, 50_000) + .Op(Instruction.POP) + .PushData(0x03) + .PushData(0x03) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(addrC, codeC); + + // Contract B: calls C + Address addrB = TestItem.AddressB; + byte[] codeB = Prepare.EvmCode + .Call(addrC, 100_000) + .Op(Instruction.POP) + .PushData(0x02) + .PushData(0x02) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(addrB, codeB); + + // Contract A: calls B (this is what the tx calls) + Address addrA = new("0x0000000000000000000000000000000000000042"); + byte[] codeA = Prepare.EvmCode + .Call(addrB, 200_000) + .Op(Instruction.POP) + .PushData(0x01) + .PushData(0x01) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(addrA, codeA); + + long gasLimit = 500_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(addrA) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().BeGreaterThan(0, "Gas estimation should succeed for deeply nested call chain"); + err.Should().BeNull("No error for deeply nested calls"); + } + + [Test] + public void Should_estimate_gas_for_nested_create2_with_inner_revert_in_constructor() + { + // CREATE2 deploys a contract whose constructor calls an external contract that reverts. + // Constructor catches the revert and continues. This is the GnosisSafe pattern: + // createProxyWithNonce -> CREATE2 -> proxy constructor -> setup() call -> possible revert + using TestEnvironment testEnvironment = new(); + + // External contract that always reverts with data + Address externalAddress = TestItem.AddressB; + byte[] externalCode = Prepare.EvmCode + .StoreDataInMemory(0, new byte[] { 0xDE, 0xAD }) + .Revert(2, 0) + .Done; + testEnvironment.InsertContract(externalAddress, externalCode); + + // Runtime code (what the proxy becomes after deployment) + byte[] runtimeCode = Prepare.EvmCode + .Op(Instruction.STOP) + .Done; + + // Init code: calls external (reverts, caught), then returns runtime code + byte[] initCode = Prepare.EvmCode + .Call(externalAddress, 20_000) // will revert, returns 0 + .Op(Instruction.POP) // discard failure result + .ForInitOf(runtimeCode) + .Done; + + // Factory: CREATE2 with salt, verify address is non-zero, STOP + Address factoryAddress = TestItem.AddressC; + byte[] factoryCode = Prepare.EvmCode + .Create2(initCode, new byte[] { 0xAB, 0xCD }, 0) // CREATE2 with salt + .Op(Instruction.POP) + .PushData(0x01) + .PushData(0x00) + .Op(Instruction.SSTORE) // record success + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(factoryAddress, factoryCode); + + long gasLimit = 500_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(factoryAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ConstantinopleFixBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().BeGreaterThan(0, "Gas estimation should succeed for CREATE2 with inner revert in constructor"); + err.Should().BeNull("No error for GnosisSafe-like CREATE2 pattern"); + } + + [Test] + public void Should_return_revert_error_when_top_level_call_reverts_with_data() + { + // Ensures gas estimation properly reports revert data when the top-level call reverts. + using TestEnvironment testEnvironment = new(); + + Address contractAddress = TestItem.AddressB; + // Store revert reason in memory, then REVERT with it + byte[] contractCode = Prepare.EvmCode + .StoreDataInMemory(0, new byte[] { 0x08, 0xC3, 0x79, 0xA0 }) // Error(string) selector + .Revert(4, 0) + .Done; + testEnvironment.InsertContract(contractAddress, contractCode); + + long gasLimit = 300_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(contractAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().Be(0, "Gas estimation should fail when top-level call reverts"); + err.Should().NotBeNull("Should report an error when top-level reverts"); + // The error contains the revert data (hex-encoded output from the REVERT opcode) + testEnvironment.tracer.TopLevelRevert.Should().BeTrue("TopLevelRevert should be set for top-level REVERT"); + } + + [Test] + public void Should_estimate_gas_with_delegatecall_that_reverts_internally() + { + // DELEGATECALL that reverts internally - the revert happens in the caller's context + // but at a nested level. Gas estimation should still succeed. + using TestEnvironment testEnvironment = new(); + + // Implementation that reverts + Address implAddress = TestItem.AddressB; + byte[] implCode = Prepare.EvmCode + .Revert(0, 0) + .Done; + testEnvironment.InsertContract(implAddress, implCode); + + // Proxy: DELEGATECALL to impl (reverts), catches it, then succeeds + Address proxyAddress = TestItem.AddressC; + byte[] proxyCode = Prepare.EvmCode + .DelegateCall(implAddress, 30_000) + .Op(Instruction.POP) // discard result + .PushData(0x01) + .PushData(0x00) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + testEnvironment.InsertContract(proxyAddress, proxyCode); + + long gasLimit = 300_000; + Transaction tx = Build.A.Transaction + .WithGasLimit(gasLimit) + .WithTo(proxyAddress) + .WithSenderAddress(TestItem.AddressA) + .TestObject; + Block block = Build.A.Block + .WithNumber(MainnetSpecProvider.ByzantiumBlockNumber + 1) + .WithTransactions(tx) + .WithGasLimit(gasLimit) + .TestObject; + + long result = testEnvironment.estimator.Estimate(tx, block.Header, testEnvironment.tracer, out string? err); + + result.Should().BeGreaterThan(0, "Gas estimation should succeed when DELEGATECALL reverts but caller handles it"); + err.Should().BeNull("No error for caught DELEGATECALL revert"); + } + private class TestEnvironment : IDisposable { public ISpecProvider _specProvider;