Skip to content

fixes#11343

Merged
svlachakis merged 16 commits into
masterfrom
fixes
Apr 27, 2026
Merged

fixes#11343
svlachakis merged 16 commits into
masterfrom
fixes

Conversation

@svlachakis
Copy link
Copy Markdown
Contributor

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @svlachakis's task in 5m 14s —— View job


PR Review

  • Gather context and read rule files
  • Read changed files
  • Analyze findings
  • Post inline comments
  • Post final review summary

Summary

This PR fixes several real bugs and adds defensive limits to the JavaScript tracer. The changes are well-scoped. A few issues need attention before merge.


What's fixed (well done)

Change File
V8 old-space capped at 128 MB — prevents JS tracers from exhausting process memory Engine.cs
Step limit (200 k/tx) + 5 s timeout on result() — prevents infinite loops and hangs GethLikeJavaScriptTxTracer.cs
Nonce override > ulong.MaxValue now rejected with a clear ArgumentException StateOverridesExtensions.cs
nonce < accountOverride.Noncenonce < newNonce (consistency fix, not a functional regression since Nonce is already checked non-null) StateOverridesExtensions.cs
PrevRandao made nullable; removed && PrevRandao != Hash256.Zero guard that silently dropped zero-hash overrides BlockOverride.cs
GasLimit overflow error message reported ulong.MaxValue instead of the actual limit long.MaxValue BlockOverride.cs

Findings

Medium

1. _resultConstructed = true moved into finally — changes disposal semantics on timeout
GethLikeJavaScriptTxTracer.cs, line 79 (inline comment)

Previously, _resultConstructed was only set on a successful result() call. Dispose() uses this flag to decide whether to call _blockTracer.Dispose() as a safety net. Moving the assignment into finally means the timeout/interrupt case now skips that cleanup path. Engine cleanup in the timeout scenario now depends entirely on the caller disposing GethLikeBlockJavaScriptTracer. This is safe where using is used correctly, but it silently removes a defensive layer that was there for exactly this failure path.

2. Missing test for PrevRandao = Hash256.Zero override
EthSimulatePrevRandaoTests.cs, line 55 (inline comment)

The actual bug fixed was that PrevRandao == Hash256.Zero was being silently skipped. Neither test covers overriding to zero — the closest is "no override at all". Adding a [TestCase] with the zero hash directly tests the regression that was fixed.

3. New test files instead of adding to existing files
StateOverridesNonceTests.cs, line 1 (inline comment)

Repo rule (test-infrastructure.md): "Add tests to existing test files rather than creating new ones." Both new test files should be merged into existing counterparts.

Low

4. using IDisposable _ = cts.Token.Register(...) — misleading variable name
GethLikeJavaScriptTxTracer.cs, line 73 (inline comment)

_ conventionally signals a discard in C#, but this value is actively used — using disposes the registration to prevent a stale interrupt if result() returns before the 5 s timer fires. Renaming to registration makes the intent clear.

5. Copyright year 2024 in both new test files
Should be 2026.


Not blocking but worth noting

  • _stepCount non-atomic: ++_stepCount is not thread-safe, but since SetOperationStack is always called from the single EVM execution thread, this is fine in practice.
  • fault not called on step-limit breach: When MaxStepCount is exceeded, InvalidOperationException propagates directly — the JS tracer's fault() callback is never invoked. Geth's behaviour here may differ; worth verifying if spec-compatibility matters.
  • V8 runtime is process-global: _runtime is static, so the 128 MB constraint is shared across all concurrent JS traces. Under high parallelism the heap limit may be hit sooner than expected for any single trace.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @svlachakis's task in 8m 27s —— View job


PR Review

  • Gather context and read rules
  • Read changed files
  • Analyze correctness, edge cases, security, performance
  • Post inline comments
  • Write final review summary

Summary

This PR bundles four independent fixes/improvements:

  1. BlockOverride.PrevRandao null-vs-zero fix — changing from Hash256 PrevRandao = Keccak.Zero to Hash256? PrevRandao and removing the != Hash256.Zero guard. Previously it was impossible to explicitly override prevrandao to Hash256.Zero. The fix is correct.
  2. BlockOverride GasLimit error message — was reporting ulong.MaxValue instead of long.MaxValue in the overflow exception. Correct.
  3. Nonce override validation + nonce < accountOverride.Nonce cleanup — adds a guard rejecting nonces > ulong.MaxValue (consistent with Geth's hexutil.Uint64), and fixes the comparison to use the already-extracted newNonce variable. The old comparison (nonce < accountOverride.Nonce) was functionally equivalent (nullable UInt256 lifts to value comparison when non-null), but this is a clarity improvement.
  4. JS tracer DoS guards — adds a per-transaction EVM step limit (200k) and a 5s timeout on the result() call with V8 interrupt, plus a 128 MiB V8 old-gen heap cap. Good protective measures, but see medium findings below.

Test coverage is solid: StateOverridesNonceTests exercises boundary nonces, EthSimulatePrevRandaoTests exercises both override and no-override paths for PREVRANDAO, and the step-limit test uses an infinite-loop bytecode.


Findings

Severity Count
Critical 0
High 0
Medium 2
Low 2

Medium

M1 — _resultConstructed = true in finally changes early-cleanup semantics (line 79, GethLikeJavaScriptTxTracer.cs)

When _tracer.result() is interrupted, the finally sets _resultConstructed = true even though the result was never produced. Dispose() then skips _blockTracer.Dispose(), deferring all V8 engine teardown to the outer using statement rather than triggering it immediately. There is no resource leak (the block tracer's Dispose() uses Interlocked.Exchange and is idempotent), but the behaviour is different from the original code and means engines are held longer under the timeout path. Either revert to setting the flag only on success, or add a comment explaining the intent.

M2 — MaxStepCount = 200_000 may be too restrictive for real-world traces (line 20, GethLikeJavaScriptTxTracer.cs)

The PR's own test comment notes that a 5M-gas tight loop generates ~1.25M step callbacks, meaning the 200k limit fires at roughly 16% of the block gas limit. Complex DeFi transactions traced with a custom step function will fail. Consider raising the default (1M–5M) or making it configurable via GethTraceOptions, and document the rationale.

Low

L1 — V8MaxOldSpaceMb = 128 lacks a comment (line 42, Engine.cs)

This is a process-global V8 heap budget shared across all concurrent JS traces. Per repo style, constraints like this should document units and rationale.

L2 — MaxNonce field placed between methods (line 115, StateOverridesExtensions.cs)

private static readonly UInt256 MaxNonce should be grouped with the other fields at the top of the class, not inserted between method bodies.


Comment thread src/Nethermind/Nethermind.Evm.Test/StateOverridesNonceTests.cs Outdated
Comment thread src/Nethermind/Nethermind.Evm/StateOverridesExtensions.cs Outdated
@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @svlachakis's task in 4m 39s —— View job


PR Review

  • Gather context
  • Read rule files
  • Read changed files
  • Analyze findings
  • Post inline comments
  • Post final review summary

What was addressed since the previous review

Previous Finding Status
MaxStepCount = 200_000 too low for real traces ✅ Raised to 1_000_000; doc comment updated
Missing prevrandao_with_zero_override_returns_zero test ✅ Added — zero hash override is now tested
using IDisposable _ misleading variable name ✅ Renamed to registration
MaxNonce field placed between methods ✅ Moved to top of class
_resultConstructed = true in finally (disposal semantics) ✅ Not in finally — correctly placed after result(), so an interrupt exception leaves the flag false and Dispose() still triggers _blockTracer.Dispose()
Copyright year 2024 in new test files ✅ Fixed to 2026 in StateOverridesNonceTests.cs

Remaining findings

Medium

M1 — New test file (StateOverridesNonceTests.cs) where none existed before
(discussion_r3137645504)

Per test-infrastructure.md: "Add tests to existing test files rather than creating new ones." There is no pre-existing unit-level StateOverrides test class in Nethermind.Evm.Test, so a new file has some justification. If kept, the class name StateOverridesNonceTests is too narrow — rename to StateOverridesTests so future tests can be added without creating yet another file.

Low

L1 — V8MaxOldSpaceMb = 128 lacks a comment
(discussion_r3137644610)

This is a process-global heap cap shared across all concurrent JS-tracer instances. Per repo style, non-obvious constants should document units and rationale.

L2 — fault() not invoked on step-limit breach
(discussion_r3137646238)

Geth calls fault() before halting on step limit. Custom tracers relying on fault() for cleanup/logging will behave differently here. Low impact in practice, but worth a code comment if spec-compatibility matters.


Not blocking

  • _stepCount is a non-atomic int — safe because SetOperationStack is always called from the single EVM execution thread.
  • Timeout string on GethTraceOptions is not wired to the 5 s JS tracer timeout — existing gap, not introduced by this PR.
  • V8 runtime is process-global: the 128 MiB cap is shared, so aggressive parallelism could hit it before the per-trace step limit fires.

Summary: The substantive fixes (PrevRandao nullable, GasLimit error message, nonce bounds check, JS tracer DoS guards) are all correct. Previous medium issues have been resolved. The one remaining medium finding (new test file / class naming) is a convention issue, not a correctness problem.

Comment thread src/Nethermind/Nethermind.Evm.Test/StateOverridesNonceTests.cs Outdated
@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude changed to timeout approach, I think it's better. Also for your other comment, When the timeout fires, Engine.Interrupt() causes step() to throw without invoking fault(). Geth exhibits the same behaviour on timeout (evm.Cancel skips fault), so this should be fine shouldn't it?

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @svlachakis's task in 2m 37s —— View job


Review update

  • Read updated files
  • Answer question about fault() / Geth behavior
  • Review the new timeout approach

Answer: fault() on timeout — you're correct

Yes, the behavior is consistent with Geth. When Engine.Interrupt() fires, V8 raises a native script interruption exception during step(). This propagates out of the EVM loop as a non-EVM exception, so ReportOperationError is never called and _tracer.fault() is never invoked. In Geth's Go JS tracer, evm.Cancel() also terminates the EVM loop without invoking the fault hook. The previous finding on this no longer applies now that the step-count mechanism is gone.


Timeout approach — what changed

Replacing the opcode step counter with a CancellationTokenSource is a better design: it bounds actual wall-clock time rather than a proxy metric. options.Timeout is now a proper TimeSpan? (was string), the test uses a 1 ms timeout on an infinite loop, and _resultConstructed = true is correctly placed after result() so disposal semantics are preserved.


One issue: indentation regression on line 67

CreateTrace() lost its indentation — it sits at column 0:

// line 67 — wrong
protected override GethLikeTxTrace CreateTrace() => new(_engine);

// should be
    protected override GethLikeTxTrace CreateTrace() => new(_engine);

Fix this →


Minor notes (non-blocking)

CTS registration not explicitly deregistered on success: _cts.Token.Register(...) return value is discarded, so the callback can't be explicitly unregistered when result() finishes early. If BuildResult() returns at 4.9 s and Dispose() is slow to be called, the interrupt fires after the result is already built. In practice this is safe — V8Engine.Interrupt() on an idle engine is a no-op — but the previous using IDisposable registration = ... pattern was more precise. Not a blocker.

V8MaxOldSpaceMb still lacks a comment: This has been flagged in two previous reviews. The constant represents a process-global V8 heap cap shared across all concurrent traces — worth one line of explanation for future maintainers.


Status of all previous findings

Finding Status
_resultConstructed = true in finally ✅ Fixed — correctly placed after result()
using IDisposable _ misleading name ✅ N/A — removed with step-count approach
MaxStepCount too low ✅ Removed — replaced by real-time timeout
fault() not called on step-limit breach ✅ Moot — step limit removed; timeout matches Geth behavior
GethTraceOptions.Timeout typed as string ✅ Fixed to TimeSpan?
Zero PrevRandao override test ✅ Added
StateOverridesTests class name ✅ Renamed from StateOverridesNonceTests
V8MaxOldSpaceMb needs comment ⚠️ Still missing
Indentation regression on CreateTrace() 🔴 New — line 67 at column 0

The only things left are the formatting regression (should be fixed before merge) and the missing comment on the heap cap (low priority).

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again, comment for V8MaxOldSpaceMb is not needed

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @svlachakis's task in 3m 33s —— View job


PR Review

  • Gather context
  • Read changed files
  • Analyze findings
  • Post final review summary

Status of all prior findings

Finding Status
CreateTrace() indentation regression ✅ Fixed
StateOverridesTests class name ✅ Fixed (renamed from StateOverridesNonceTests)
Zero PrevRandao override test ✅ Added
_resultConstructed = true placement ✅ Fixed (after result(), not in finally)
fault() not called on timeout ✅ Acknowledged — matches Geth evm.Cancel behaviour
using IDisposable _ misleading name ✅ Fixed — now _ctsRegistration
MaxNonce field between methods ✅ Fixed — moved to top of class
V8MaxOldSpaceMb needs a comment User says not needed — not flagged

Remaining finding

Medium — GethTraceOptions.Timeout type change breaks API deserialization

GethTraceOptions.cs line 24: public TimeSpan? Timeout

The old string Timeout field accepted any JSON string value (including Geth's "5s" / "200ms" format), even though it was unused. Changing to TimeSpan? without a custom JSON converter causes System.Text.Json to reject Geth-style duration strings — it expects ISO 8601 (e.g. "00:00:05"), not Go time.Duration format.

Any client or tool (Foundry, Hardhat, ethers, or anything wrapping Geth-compatible debug_traceTransaction) that was passing {"timeout": "5s"} will now receive a deserialization error instead of the parameter being silently ignored. This is a public API regression.

Fix: add a custom converter that parses both formats:

[JsonConverter(typeof(GoDurationConverter))]
public TimeSpan? Timeout { get; init; }

Or parse the Go duration string in the setter when the value is a string. At minimum, document that the format expected is ISO 8601, not Go's time.Duration.


Not blocking

  • _ctsRegistration.Dispose() is called in both BuildResult() (line 76) and Dispose() (line 261). This is intentional and safe — CancellationTokenRegistration.Dispose() is idempotent. Early dispose in the success path prevents a stale interrupt firing after the result is built; the Dispose() call is the defensive fallback for error paths.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @svlachakis's task in 2m 5s —— View job


PR Review

  • Gather context
  • Read rule files
  • Read changed files
  • Analyze findings
  • Post final review summary

Status of all prior findings

Finding Status
_resultConstructed = true placement ✅ Fixed — set after result(), not in finally
_ctsRegistration naming ✅ Correct — disposed in both success path (line 81) and Dispose() (line 266); idempotent
GethTraceOptions.Timeout type change breaks JSON API ✅ Fixed — string?, parsed via TimeSpan.TryParse, throws on invalid
Doc comment said "unparseable" but code throws ✅ Fixed — now says "null or empty" and "Throws ArgumentException for invalid values"
Zero PrevRandao override test ✅ Covered — prevrandao_with_zero_override_returns_zero test present
StateOverridesTests class name and copyright ✅ Correct — StateOverridesTests, copyright 2026
MaxNonce field placement ✅ At top of class
Indentation regression on CreateTrace() ✅ Fixed
Timeout test removed ✅ Accepted — flaky wall-clock tests are not worth restoring
V8MaxOldSpaceMb comment ✅ Not needed per author

All findings from previous rounds have been resolved. The implementation is correct:

  • Timeout via CancellationTokenSource covers all JS execution (step, enter, exit, result), not just result() — this is intentional and correct.
  • _ctsRegistration.Dispose() early in the success path prevents a stale interrupt firing after the result is built; the Dispose() call is the safe fallback.
  • static e => ((Engine)e!) avoids a closure allocation — correct pattern.

This PR is ready to merge.

@svlachakis svlachakis marked this pull request as ready for review April 24, 2026 13:09
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @svlachakis's task in 1m 52s —— View job


PR Review

  • Gather context
  • Read rule files and changed files
  • Analyze findings
  • Post final review summary

Status of all prior findings

Finding Status
_resultConstructed = true placement ✅ Set after result(), not in finally — correct
_ctsRegistration naming and disposal ✅ Named _ctsRegistration, disposed in both BuildResult() success path and Dispose() — idempotent and correct
MaxStepCount too low / removed ✅ Removed, replaced by wall-clock timeout
fault() not called on timeout ✅ Acknowledged — matches Geth evm.Cancel behaviour
GethTraceOptions.Timeout type breaks JSON API string?, parsed via TimeSpan.TryParse, throws on invalid
Doc comment says "unparseable" but code throws ✅ Fixed — now correctly says "Throws ArgumentException for invalid values"
Zero PrevRandao override test prevrandao_with_zero_override_returns_zero in EthSimulateTestsBlocksAndTransactions.cs
StateOverridesTests class name and file ✅ Correct name, correct copyright year 2026
MaxNonce field placement ✅ At top of class
Indentation regression on CreateTrace() ✅ Fixed
V8MaxOldSpaceMb needs comment Explicitly declined by author — not flagged

No remaining findings

All substantive correctness fixes are correct and complete:

  • PrevRandao nullable — zero-hash overrides no longer silently dropped
  • GasLimit error message — now reports long.MaxValue correctly
  • Nonce bounds validation — rejects > ulong.MaxValue with a clear ArgumentException
  • JS tracer DoS guard — wall-clock timeout with V8 interrupt, _resultConstructed flag preserves disposal semantics correctly

This PR is ready to merge.

Comment thread src/Nethermind/Nethermind.Blockchain/Tracing/GethStyle/GethTraceOptions.cs Outdated
@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude resolved @LukaszRozmej's comments, added geth format support alongside c# time format support. review again please

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @svlachakis's task in 2m 41s —— View job


PR Review

  • Gather context
  • Read rule files and changed files
  • Analyze findings
  • Post final review summary

What was addressed since the previous review

Previous finding Status
GethTraceOptions.Timeout type breaks JSON API (rejects Geth "5s" format) ✅ Fixed — CustomTimeDurationConverter accepts both Go format and C# TimeSpan format
LukaszRozmej: "Is this format compatible with Geth?" ✅ Yes — CustomTimeDurationConverter ports Go's time.ParseDuration exactly, including ns/µs/ms/s/m/h units, fractional values, and sign prefix
LukaszRozmej: "should parsed format have some kind of cap?" ✅ Fixed — MaxTimeout = TimeSpan.FromMinutes(2) enforced in the constructor with a clear error

New code — CustomTimeDurationConverter

The implementation is a faithful port of Go's time.ParseDuration / Duration.String():

  • Read: tries Go format first, falls back to TimeSpan.TryParse (ISO 8601 / hh:mm:ss). Throws JsonException on anything unrecognisable.
  • Write: serialises using Go's Duration.String() format (right-to-left stackalloc buffer, trailing-zero suppression) — round-trips cleanly.
  • Both µs (U+00B5) and μs (U+03BC) accepted, matching Go's unitMap.
  • "0" accepted without a unit (matches Go's special case).
  • Overflow guard mirrors Go's int64 nanosecond cap (1 << 63).
  • Tests cover: valid Go durations, valid C# TimeSpan, invalid inputs, write round-trips. Comprehensive.

Constructor validation

TimeSpan timeout = options.Timeout ?? DefaultTimeout;
if (timeout <= TimeSpan.Zero || timeout > MaxTimeout)
    throw new ArgumentOutOfRangeException(...);
_cts = new CancellationTokenSource(timeout);
_ctsRegistration = _cts.Token.Register(static e => ((Engine)e!).Interrupt(), engine);
  • Zero and negative values (Go -5s parses to a negative TimeSpan, correctly rejected). ✅
  • Values above 2 minutes rejected. ✅
  • DefaultTimeout = 5s is never validated (not needed — 5s < MaxTimeout). ✅

Disposal chain

  • BuildResult() success path: _ctsRegistration.Dispose() before _resultConstructed = true — prevents stale interrupt. ✅
  • Dispose(): _ctsRegistration.Dispose() + _cts.Dispose() — both are idempotent. ✅
  • if (!_resultConstructed) _blockTracer.Dispose() — eagerly releases V8 engines on failure. ✅

No remaining findings

Category Count
Critical 0
High 0
Medium 0
Low 0

All substantive fixes (PrevRandao nullable, GasLimit error message, nonce bounds check, JS tracer wall-clock timeout with Geth-compatible format support and 2-minute cap) are correct and complete. This PR is ready to merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@LukaszRozmej
Copy link
Copy Markdown
Member

CustomTimeDurationConverter — refactor notes (commit e72b6fd)

Zero-allocation JSON paths

  • Read: Utf8JsonReader.CopyString into a 64-char stack buffer instead of reader.GetString() (heap alloc). Uses HasValueSequence ? ValueSequence.Length : ValueSpan.Length — UTF-8 byte count is a safe upper bound on UTF-16 char count. The stackalloc lives in its own helper (ReadFromStackBuffer) so the buffer is only reserved and zero-initialized on the path that actually uses it; fast exits (null token, byteLen > 64) pay nothing. Fallback to GetString() for the rare oversized case.
  • Write: Format now writes into a caller-provided Span<char> and returns ReadOnlySpan<char> fed directly into Utf8JsonWriter.WriteStringValue(ReadOnlySpan<char>) — no new string(buf[w..]) allocation. Same helper pattern as Read (WriteFormatted) so the format buffer is only reserved when value is not null.

Unit dictionary → SearchValues + switch expression

  • Replaced Dictionary<string, ulong> UnitMap (hashing + string allocation for the unit substring) with SearchValues<char> UnitTerminators = SearchValues.Create(".0123456789") + a switch expression over ReadOnlySpan<char>. No allocations, no hashing, no s[unitStart..i] substring.
  • Unit-end scan while (i < s.Length && s[i] != '.' && !IsDigit(s[i])) i++;s.IndexOfAny(UnitTerminators) (SIMD-capable one-liner).

Correctness fix in integer overflow check

Previous guard was v > ulong.MaxValue / 10 before the multiply, which allowed v * 10 + digit to wrap when the digit was 6-9 (since (ulong.MaxValue / 10) * 10 + 9 overflows past ulong.MaxValue). Now uses Go's 1<<63 cap with a post-check — matches Go exactly.

Fractional math: doubleUInt128

Go uses float64 * (float64 / scale) with a comment noting it must be nanosecond-accurate for fractions of an hour. Switched to (UInt128)f * unitNs / scale — exact integer arithmetic, no rounding, same result for representable values.

Span-first parsing

  • TryParseGoDuration takes ReadOnlySpan<char> instead of string; segments are consumed by slicing (s = s[1..]) rather than substring.
  • Digit runs are bounded with s.IndexOfAnyExcept(Digits) (SIMD-capable) via a TakeDigits helper, so accumulation loops get clean slices to iterate over.
  • Integer part: ulong.TryParse with built-in overflow detection, then one v <= MaxNs bound check.
  • Fractional part: cap at 18 digits (always fits ulong) and look up scale via a Pow10 table. Go accumulates 18-19 digits depending on value; we always truncate at 18. The difference is below the 100 ns tick boundary.

Readability cleanups

  • Per-segment parsing split into TryParseSegment + TryTakeInteger / TakeFraction / TryTakeUnit / TakeDigits helpers — each does one thing.
  • Named constants replace magic numbers: NsPerTick = 100, SecondsPerMinute = 60, MinutesPerHour = 60, NsDigitsInMicrosecond/Millisecond/Second = 3/6/9, FormatBufferSize = 32, ReadStackBufferSize = 64, MaxFractionalDigits = 18, MaxNs = 1UL << 63.
  • Sub-second unit/precision selection in Format collapsed to a tuple switch on u.
  • Consistent buf[--w] = c (replaces split w--; buf[w] = c).
  • result = defaultresult = TimeSpan.Zero.
  • TakeFraction captures ulong.TryParse return with a Debug.Assert to document that parsing cannot fail on ASCII-digit input capped at 18 chars.

18 new test cases

Added edge cases targeting paths the refactor touched:

Valid parse (6): "1.s" (trailing dot, no fraction), ".5s" (leading dot, no integer), "0.5h" (fractional × hour — the UInt128 path), "0.123456789s" (9-digit precision), "-1h30m" (negative multi-segment), "05s" (leading zeros — the ulong.TryParse change).

Invalid parse (4): "+", "1.2.3s", "5 s", "1ss".

Write (5): FromMilliseconds(-1500)"-1.5s", FromMicroseconds(-500)"-500µs", FromTicks(1)"100ns", FromHours(1)"1h0m0s", FromSeconds(60)"1m0s".

Round-trip (3): negative, FromMicroseconds(1500) (µs → ms cross-unit), FromHours(24).

Verification: ran all 60 tests against both the pre-refactor and post-refactor implementations — both pass all of them, confirming the refactor is behavior-preserving across every edge case we identified.

@LukaszRozmej
Copy link
Copy Markdown
Member

@claude re-review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 24, 2026

Claude finished @LukaszRozmej's task in 5m 24s —— View job


PR Review

  • Gather context
  • Read rule files
  • Read changed files
  • Analyze findings
  • Post final review summary

CustomTimeDurationConverter — deep review

This is the main new addition. I verified the refactored implementation against LukaszRozmej's notes line by line:

Property Status
CopyString into 64-char stack buffer instead of GetString() — zero allocation for short strings ✅ Lines 72–75
Format writes into caller-provided Span<char>, fed directly to WriteStringValue(ReadOnlySpan<char>) — zero allocation ✅ Lines 91–95
SearchValues<char> for SIMD-capable IndexOfAny/IndexOfAnyExcept scans ✅ Lines 44, 47, 276, 287
switch expression over ReadOnlySpan<char> replaces Dictionary hashing ✅ Lines 300–309
UInt128 for fractional math — exact integer arithmetic, no float rounding ✅ Line 241
ulong.TryParse with built-in overflow detection on integer parts ✅ Line 252
Named constants replacing magic numbers (NsPerTick, MaxNs, FormatBufferSize, etc.) ✅ Lines 18–42
Pow10 table avoids recomputation for fractional scaling ✅ Lines 50–57
TryParseSegment / TryTakeInteger / TakeFraction / TryTakeUnit / TakeDigits each do one thing
format writes right-to-left, suppresses trailing zeros — matches Go's Duration.String() ✅ Lines 99–148
Both µ (U+00B5) and μ (U+03BC) accepted ✅ Line 303
"0" without unit accepted as Go special case ✅ Line 189
Positive-value cap: totalNs > MaxNs - 1 matches Go's d > 1<<63-1 ✅ Line 206
Per-segment overflow guard: v > MaxNs / unitNs before multiply ✅ Line 234
MaxFractionalDigits = 18 always fits ulong 2^63 >> 10^18
FormatBufferSize = 32, max output is 25 chars
Debug.Assert documents the invariant on TryParse of known-digit input ✅ Line 270
60 test cases: valid Go formats, valid C# TimeSpan, invalid inputs, write serialization, round-trips

Everything checks out. The implementation is a faithful, well-tested port of Go's duration format.


All prior findings — resolved

Finding Status
_resultConstructed = true in finally (disposal semantics) ✅ Set after result(), not in finally — correct
_ctsRegistration naming and dual-dispose ✅ Disposed in BuildResult() (prevents stale interrupt) and Dispose() (idempotent fallback)
MaxStepCount too low ✅ Removed — replaced by wall-clock timeout
GethTraceOptions.Timeout type breaks Geth JSON clients [JsonConverter(typeof(CustomTimeDurationConverter))] on TimeSpan? handles both "5s" and ISO 8601
Negative durations and zero rejected ✅ Constructor guards timeout <= TimeSpan.Zero || timeout > MaxTimeout
Zero PrevRandao override silently ignored PrevRandao is Hash256?, && PrevRandao != Hash256.Zero guard removed
Zero PrevRandao test prevrandao_with_zero_override_returns_zero test case
GasLimit error message reported ulong.MaxValue ✅ Now correctly reports long.MaxValue
Nonce > ulong.MaxValue not validated MaxNonce = ulong.MaxValue at top of class, check added
MaxNonce field between methods ✅ At top of class
StateOverridesTests class name and file ✅ Correct class name, copyright 2026
LukaszRozmej's Geth format compatibility question CustomTimeDurationConverter ports Go's time.ParseDuration exactly
LukaszRozmej's "should it have a cap?" question MaxTimeout = TimeSpan.FromMinutes(2) enforced in constructor

No remaining findings

Severity Count
Critical 0
High 0
Medium 0
Low 0

All substantive fixes and the CustomTimeDurationConverter refactor are correct and complete. This PR is ready to merge.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@LukaszRozmej Thanks, LGTM!

@LukaszRozmej
Copy link
Copy Markdown
Member

@svlachakis please merge master

# Conflicts:
#	src/Nethermind/Nethermind.Evm/BlockOverride.cs
#	src/Nethermind/Nethermind.JsonRpc.Test/Modules/Eth/Simulate/EthSimulateTestsBlocksAndTransactions.cs
@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude I did some more refactoring please check.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 26, 2026

Claude finished @svlachakis's task in 2m 5s —— View job


PR Review

  • Gather context and read rule files
  • Read changed files
  • Analyze findings
  • Post final review summary

What changed in this refactoring

The refactoring touches only CustomTimeDurationConverter.cs and its tests. Four correctness fixes, two safety improvements, and a cleanup pass.


Correctness fixes (all correct)

1. Overflow in Format() for large TimeSpan values

Previous code: ulong u = (ulong)(neg ? -ticks : ticks) * NsPerTick;
TimeSpan.MaxValue.Ticks = 9,223,372,036,854,775,807. Multiplied by 100 (NsPerTick) this overflows ulong.MaxValue. The fix splits before scaling:

ulong seconds = absTicks / TicksPerSecond;
ulong nsFraction = (absTicks % TicksPerSecond) * NsPerTick;

nsFraction is at most 9,999,999 × 100 = 999,999,900 — well within range. The ExtremeWriteCases tests (TimeSpan.MaxValue, TimeSpan.MinValue, 10,000-day values) cover this.

2. "-0" / "+0" not parsed correctly

Previously the if (s is "0") return true; check ran before sign-stripping, so "-0" would fail (sign wasn't stripped yet). Moving the check after sign-stripping fixes it. The new negative_zero and positive_zero test cases cover this.

3. Silent ulong wrap in segment accumulation

Previous: totalNs += segmentNs; if (totalNs > MaxNs) return false; — if totalNs + segmentNs overflowed ulong, the > MaxNs check would see a small wrapped value and pass incorrectly.

New: if (segmentNs > MaxNs - totalNs) return false; totalNs += segmentNs; — pre-checks before adding. The multi_segment_overflow_wraps_ulong test case ("9223372036854775808ns9223372036854775808ns") covers this.

4. CultureInfo.InvariantCulture on TimeSpan.TryParse

The C# fallback parser was using the current thread culture, which could differ on machines with non-invariant locale settings. Added CultureInfo.InvariantCulture — correct.


Safety improvements (both correct)

5. Non-string token type check — new if (reader.TokenType != JsonTokenType.String) throw new JsonException(...) guards against 123, true, {}, [] inputs with a clear error. Tests added.

6. Max-input-length throws instead of falling back to GetString() — inputs over 64 chars now throw a JsonException immediately. The old path allocated a heap string for oversized inputs; this is cleaner and provides a DoS guard. Test added.


Code clarity

Named variables (seconds, minutes, hours) replace re-use of the u accumulator across the Format() branches — clearer and matches the new split-before-scaling structure. Comment trimming is in keeping with repo style (removed WHAT comments, kept WHY comments for non-obvious invariants).


No findings

Severity Count
Critical 0
High 0
Medium 0
Low 0

All four correctness fixes are sound, test coverage is comprehensive (correctness, edge cases, overflow, non-string tokens, extreme TimeSpan values), and the code reads cleanly.

This PR is ready to merge.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@LukaszRozmej I did some more refactoring on the go converter, please check again

@svlachakis svlachakis merged commit 18d60a4 into master Apr 27, 2026
434 of 435 checks passed
@svlachakis svlachakis deleted the fixes branch April 27, 2026 11:08
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.

4 participants