Skip to content

refactor(GasEstimator): Separate OptimisticMultiplier from stop margin#12088

Open
manusw7 wants to merge 6 commits into
masterfrom
fix/rpc-gas-estimator-optimistic-multiplier
Open

refactor(GasEstimator): Separate OptimisticMultiplier from stop margin#12088
manusw7 wants to merge 6 commits into
masterfrom
fix/rpc-gas-estimator-optimistic-multiplier

Conversation

@manusw7

@manusw7 manusw7 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #12087

Changes

  • Introduce OptimisticMultiplier = 64d / 63d in GasEstimator.cs to compensate for EIP-150's 1/64 child-call reservation, matching Geth's approach
  • Pass OptimisticMultiplier to TryOptimisticEstimate instead of marginMultiplier, keeping marginMultiplier for the binary-search stop threshold only
  • Fix IsSimpleTransfer to return false for EIP-7702 SetCodeTx with an authorizationList, ensuring those transactions go through the binary search

Types of changes

  • Bugfix (a non-breaking change that fixes an issue)

Testing

Requires testing

  • Yes

If yes, did you write tests?

  • Yes

Notes on testing

Two unit tests added to GasEstimationTests.cs:

  • Estimate_subcall_contract_uses_optimistic_multiplier_not_margin_multiplier — verifies the optimistic guess correctly accounts for EIP-150's child-call budget
  • IsSimpleTransfer_returns_false_for_SetCodeTx_with_authorization_list — verifies SetCodeTx goes through the full binary search

Documentation

Requires documentation update

  • No

Requires explanation in Release Notes

  • No

@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @manusw7's task in 3m 40s —— View job


Code Review

  • Load tools
  • Read changed files and rule files
  • Analyze correctness, edge cases, security, performance
  • Post inline comments
  • Write final review summary

Summary

The production code changes are correct: separating OptimisticMultiplier = 64d / 63d from marginMultiplier is a valid fix for the EIP-150 interaction, and the IsSimpleTransfer fix for EIP-7702 SetCode transactions is clearly necessary. However, one of the two tests added doesn't actually validate the specific behavior it claims to, which means a regression would go undetected.


Findings

Medium

Estimate_subcall_contract_uses_optimistic_multiplier_not_margin_multiplier doesn't verify the fix (line 1257–1297)

The assertions err is null + result > 0 pass with the old code too, since the old marginMultiplier ≈ 1.015 and new OptimisticMultiplier ≈ 1.01587 are nearly indistinguishable for a simple two-call contract. The test cannot catch a regression that reverts the fix.

The scenario where the split matters is errorMargin = 0: the old code computed optimistic = gasSpent * 1.0 (no EIP-150 correction), while the fixed code always applies 64/63. A stronger test should pass errorMargin: 0 and assert the result is still correct. See inline comment for details.


Low

  1. Missing err assertion in the SetCode testIsSimpleTransfer_returns_false_for_SetCodeTx_with_authorization_list asserts result > BaseTxGasCost but never asserts err is null. See inline comment for the one-line fix.

  2. Misleading parameter name in TryOptimisticEstimate — The private method signature still reads double marginMultiplier at line 172, but it is now always called with OptimisticMultiplier (a fixed physical constant, not a margin). Consider renaming to eip150Multiplier or just multiplier to avoid confusing future readers.


Notes (no action needed)

  • OptimisticMultiplier = 64/63 compensates for a single EIP-150 nesting level. For N nested CALLs the exact correction would be (64/63)^N, so the optimistic guess can still underestimate in deeply nested contracts. This is intentional — it's an optimistic first guess that the binary search corrects — and it matches Geth's behavior exactly.
  • HasAuthorizationList correctly requires both TxType.SetCode and AuthorizationList.Length > 0, so a SetCode tx with an empty/null auth list (which is already protocol-invalid per EIP-7702) would still be caught by the tx.Data.IsEmpty condition only — acceptable since such a tx would never pass mempool validation.

@manusw7

manusw7 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @manusw7's task in 3m 12s —— View job


Code Review

  • Load tools
  • Read changed files
  • Read rule files and understand GasEstimator logic
  • Analyze correctness, edge cases, security, performance
  • Post inline comments and final review

Summary

The production-code changes are correct and well-motivated. Both the OptimisticMultiplier separation and the IsSimpleTransfer fix address real bugs. The tests need minor improvements before this fully meets the repo's regression-test bar.


Production Code (GasEstimator.cs)

OptimisticMultiplier = 64d / 63d (line 40 / line 153)

The separation is correct. The old code passed marginMultiplier (default ≈ 1.015 for 150 bp) to TryOptimisticEstimate. When errorMargin = 0, marginMultiplier = 1.0, so the optimistic guess equalled gasSpent — but for any transaction with sub-calls, EIP-150's 1/64 reservation means you need slightly more gas than what was consumed at the call site. Using 64/63 ≈ 1.01587 always provides this headroom, independent of the configured error margin. This matches Geth's approach.

IsSimpleTransfer fix (line 221)

HasAuthorizationList is only true when Type == TxType.SetCode && AuthorizationList is not null && AuthorizationList.Length > 0, so the guard is tight. A SetCode tx with empty calldata is not a simple ETH transfer — it processes authorization tuples that may trigger EVM execution on the delegated address, requiring more gas than just intrinsicGas. The short-circuit was wrong.


Findings

Medium

Test 1 doesn't exercise the bug it's named after — see inline comment on line 1296 of GasEstimationTests.cs. With the default errorMargin = 150 bp, marginMultiplier ≈ 1.015 ≈ 64/63, so both old and new code produce a passing result. The distinguishing scenario is errorMargin = 0, where the old optimistic = gasSpent × 1.0 would be too low. Adding errorMargin: 0 to the Estimate call makes this a genuine regression guard.

Low

  • Test 2 missing err is null assertion — see inline comment on line 1324. Failing the > 21000 assertion on result = 0 gives a confusing message; surfacing the actual error string is much more diagnostic.
  • TryOptimisticEstimate parameter still called marginMultiplier (line 172, GasEstimator.cs — not in diff, so can't inline). After this PR it only ever receives OptimisticMultiplier. Renaming to optimisticMultiplier avoids misleading the next reader into thinking the search-stop threshold is passed here.

Verdict

Mergeable once the test-1 assertion is strengthened to use errorMargin: 0 — otherwise the regression test for the primary bug fix provides no protection. The remaining findings are low-severity and can be addressed here or as follow-up.

@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @manusw7's task in 5m 13s —— View job


PR Review: refactor(GasEstimator): Separate OptimisticMultiplier from stop margin

Todo list:

  • Load tools
  • Read changed files
  • Read relevant rules files
  • Analyze correctness, edge cases, security, performance
  • Post findings

Summary

The two production changes are semantically correct: separating the EIP-150 64/63 multiplier from the user-specified error margin, and excluding EIP-7702 SetCode transactions from the IsSimpleTransfer shortcut. The core logic aligns with Geth's approach and fixes a real conceptual mismatch.

However, the accompanying tests have quality issues: neither new test actually detects a regression to the previous behavior. Three Medium findings below.


Findings

Medium — TryOptimisticEstimate parameter name is now misleading

GasEstimator.cs:173 — The parameter is still named marginMultiplier, but since this PR it always receives the fixed EIP-150 constant OptimisticMultiplier, never a user-controlled margin. A future reader (or the compiler-warning tooling) will see the callee reading a marginMultiplier and the caller passing OptimisticMultiplier, which are logically distinct things.

// before
private (long Left, long Right) TryOptimisticEstimate(
    ..., double marginMultiplier, ...) { ... }

// should be
private (long Left, long Right) TryOptimisticEstimate(
    ..., double optimisticMultiplier, ...) { ... }

Fix this →


Medium — Estimate_subcall_contract_uses_optimistic_multiplier_not_margin_multiplier does not actually detect the bug

GasEstimationTests.cs:1257–1297 — The test only asserts err == null && result > 0. This would pass identically before and after the fix, because:

  • Default errorMargin = 150marginMultiplier = 1.015
  • OptimisticMultiplier = 64/63 ≈ 1.0159

Both values are so close that the binary search converges to the same final rightBound in either case. A test that actually detects the regression needs either:

  1. An errorMargin below 159 bps (e.g., 0) where the old code's optimistic guess would fall below the actual needed gas for the subcall, and a numerical assertion on the result, or
  2. An assertion using Assert.Multiple that verifies the estimate is both error-free AND within a tight range demonstrating it was correctly estimated on the first attempt.

As written, the test is an integration smoke test, not a regression guard for this specific fix.


Medium — IsSimpleTransfer_returns_false_for_SetCodeTx_with_authorization_list assertion is too weak to catch a regression

GasEstimationTests.cs:1300–1325 — The assertion is result > Transaction.BaseTxGasCost (i.e., > 21000). However, with the old code (IsSimpleTransfer returning true for this tx):

  1. IsSimpleTransfer = true, TryExecute(tx, header, intrinsicGas) is called.
  2. Intrinsic gas for SetCode with 1 auth tuple ≈ 23 500 (21 000 + 2 500 per EIP-7702).
  3. If that execution succeeds → returns 23 500, which satisfies > 21 000.

So the test passes with or without the fix. A stronger assertion would be something like:

// The SetCode auth list costs extra beyond a plain transfer; verify binary search ran
Assert.That(err, Is.Null);
Assert.That(result, Is.GreaterThan(Transaction.BaseTxGasCost), "must exceed plain transfer cost");
// Optionally: Assert.That(result, Is.GreaterThanOrEqualTo(intrinsicGas))
// to prove the simple-transfer shortcut (which returns exactly intrinsicGas) was NOT taken.

At a minimum, err should be asserted null — if estimation fails, result will be 0 and the > 21000 assertion would fail anyway, but the error message would be confusing without the err check.

Fix this →


What is correct

  • OptimisticMultiplier = 64d / 63d — Mathematically sound. EIP-150 retains floor(n/64) gas at every call boundary, so to recover the true gas needed for a subcall that consumed S gas, the caller needed S * 64/63 available. Using the user's error margin here was wrong (and silently broke for any margin < 159 bps).
  • IsSimpleTransfer + !tx.HasAuthorizationList — Correct. EIP-7702 auth-list processing is not a "simple transfer"; the delegation setup may consume gas beyond the intrinsic minimum, and the binary search is required.
  • HasAuthorizationList property usage — Correctly handles null and empty auth list via the [MemberNotNullWhen]-annotated property.
  • Comment on OptimisticMultiplier — Accurate and explains the "why" per coding guidelines.

Verdict

The production changes are correct. The test issues are Medium because the PR explicitly bills them as regression tests for the two fixes, but neither test would fail if the code were reverted. Addressing the parameter rename and strengthening the two tests is recommended before merge.

Comment thread src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs
Comment thread src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs
Comment thread src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs Outdated
Comment thread src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs
Comment thread src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs
Comment thread src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs
@manusw7

manusw7 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@claude test updated. Review again

@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @manusw7's task in 5m 27s —— View job


Code Review (updated tests)

  • Load tools
  • Read updated test file and GasEstimator.cs
  • Evaluate whether tests now properly guard against regressions
  • Read relevant rule files
  • Post findings

Summary

Both tests are genuine improvements over the previous version. The production code remains correct. One naming issue in GasEstimator.cs flagged in prior reviews is still unaddressed.


Test 1 — Estimate_subcall_contract_uses_optimistic_multiplier_not_margin_multiplier

Improvement confirmed ✅errorMargin: 0 was added as requested. This is a meaningful change: with errorMargin: 0 the binary search converges to the exact minimum gas (no slack), which exercises the optimistic probe more strictly.

Residual note (Low) — The assertions err is null + result > BaseTxGasCost still pass with the old code. The reason: with errorMargin: 0, ShouldContinueSearch uses threshold = 0 and runs until leftBound + 1 == rightBound, so the binary search always converges to the correct minimum regardless of where the optimistic probe lands. Both old (gasSpent × 1.0) and new (gasSpent × 64/63) produce the same correct final rightBound. The distinction between the two is efficiency (fewer iterations), not the answer. This means the test cannot catch a regression that reverts the multiplier change.

That said, tightening this further without mock infrastructure (intercepting TryExecute call counts) or a scenario where old code produces a wrong answer (none found) is non-trivial. The current test is a meaningful smoke test for the subcall path with strict convergence.


Test 2 — IsSimpleTransfer_returns_false_for_SetCodeTx_with_authorization_list

Improvement confirmed ✅Assert.That(err, Is.Null, err) was added, which gives much better diagnostics on failure.

Residual note (Low)result > Transaction.BaseTxGasCost (> 21 000) remains a weak lower bound. With TestItem.AddressB as target (no deployed code), the intrinsic gas for a SetCode tx with 1 auth tuple is 21 000 + 2 500 = 23 500. Both the old code (simple-transfer shortcut) and the new code (binary search) converge to approximately 23 500, so both satisfy > 21 000. To make this a distinguishing assertion, the target would need deployed code that consumes gas beyond the intrinsic cost — then old code would return the wrong (too-low) estimate and new code would return the correct one.


Unaddressed Medium — TryOptimisticEstimate parameter name

Still marginMultiplier at line 173 of GasEstimator.cs — this was flagged in all three prior reviews:

// current
private (long Left, long Right) TryOptimisticEstimate(
    ..., double marginMultiplier, ...) { ... }

After this PR the method is always called with OptimisticMultiplier (a fixed EIP-150 physical constant). The parameter name marginMultiplier implies it carries the user-specified error margin, which it no longer does. Renaming it to optimisticMultiplier removes the ambiguity.

Fix this →


What is correct

  • OptimisticMultiplier = 64d / 63d and its call site (line 153) ✅
  • IsSimpleTransfer + !tx.HasAuthorizationList (line 221) ✅
  • HasAuthorizationList null/empty handling ✅
  • Comment on OptimisticMultiplier
  • err is null assertions in both new tests ✅

Verdict

Production code is correct and the tests are measurably better than the prior version. The one outstanding item is the parameter rename in TryOptimisticEstimate (Medium per prior reviews, Low in practice). The test limitations noted are fundamental to the estimator's design (binary search always converges correctly regardless of the probe quality) — they are acceptable as smoke tests, acknowledging they cannot be tightened without mock infrastructure or a more elaborate contract setup.

@manusw7

manusw7 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@claude review again

@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @manusw7's task in 1m 33s —— View job


Code Review (re-review)

  • Load tools
  • Read changed files (GasEstimator.cs and GasEstimationTests.cs)
  • Read relevant rule files
  • Analyze correctness, edge cases, and test coverage
  • Post findings

Summary

All previously flagged findings are now addressed. The production code is correct and the tests are adequate smoke tests for the fix paths.


What changed since the last review

TryOptimisticEstimate parameter rename — resolved ✅

The parameter at line 173 is now optimisticMultiplier (was marginMultiplier), and the usage at line 179 is updated accordingly. This was the last outstanding Medium finding from prior reviews.


Production Code — all correct ✅

Change Assessment
OptimisticMultiplier = 64d / 63d (line 40) Mathematically correct EIP-150 correction. Matches Geth.
TryOptimisticEstimate called with OptimisticMultiplier (line 153) Correctly decouples the optimistic probe from the user-configured search-stop threshold.
TryOptimisticEstimate parameter renamed to optimisticMultiplier (line 173) Now accurately reflects the constant's purpose.
IsSimpleTransfer + !tx.HasAuthorizationList (line 221) Correctly prevents EIP-7702 SetCode txs from bypassing the binary search.

Tests — acceptable ✅

Both tests are improved and pass correctly. Their inherent limitations are well-understood:

  • Test 1errorMargin: 0 is now set. The assertions (err is null + result > 21000) still pass with old code because the binary search always converges to the correct answer regardless of where the optimistic probe lands. This is a fundamental property of the estimator design, not a fixable test gap. The test validates that the subcall path works correctly at zero margin.

  • Test 2Assert.That(err, Is.Null, err) added. The result > 21000 bound is still a weak lower bound in isolation (intrinsic gas for a SetCode tx with 1 auth tuple is ≈ 23 500, so both old and new code would exceed 21 000 on an empty target). The test primarily verifies that the path succeeds without error, which is the key behavioral requirement.

Neither limitation warrants blocking the PR — they are accepted smoke tests for paths that require more complex mock infrastructure to assert on probe-level behavior.


Verdict

Mergeable. All Medium findings from prior reviews have been addressed. No new issues found.

private const double BasisPointsDivisor = 10000d;

// EIP-150: each CALL site reserves 1/64 for the caller, so the optimistic guess must use 64/63, not the search-stop margin.
private const double OptimisticMultiplier = 64d / 63d;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be covered by just using a bit different error margin? It's now 150, seems like you suggest ~158

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with that is that the two values serve different purposes
The error margin controls search accuracy and is user-configurable. The 64/63 is a fixed EVM property from EIP-150

Comment thread src/Nethermind/Nethermind.Blockchain/Tracing/GasEstimator.cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

eth_estimateGas: optimistic-try multiplier reuses the binary-search-stop margin instead of Geth's EIP-150 64/63 budget

5 participants