RPC: Debug* streaming approach - #11693
Conversation
|
Claude finished @svlachakis's task in 5m 57s —— View job PR Review: RPC Debug* Streaming ApproachTasks
SummaryThe architecture is well-designed: Two High severity issues need addressing before merge, plus several Medium findings. FindingsHigh1. Unclosed JSON array/object on cancellation — 2. Medium3. Sync-over-async on EVM thread — 4. 5. No streaming-specific tests — tests verify the final serialised JSON output via
Low6. 7. What's good
|
|
@claude review again carefully, check possible functionality regressions. |
|
Claude finished @svlachakis's task in 10m 41s —— View job PR Review: RPC Debug* Streaming Approach — Regression AnalysisTasks
SummaryThe core streaming architecture is sound, and several issues from the prior review (unclosed JSON arrays, 4 High findings and 2 Medium findings that need addressing before merge. High Severity1. JSON field order changed — breaks Geth-compatible tooling → inlineFile: Non-streaming path ( {"gas":…,"failed":…,"returnValue":"0x","structLogs":[…]}Streaming path emits: {"structLogs":[…],"gas":…,"failed":…,"returnValue":"0x"}Geth's reference implementation uses the first order. Any conformance tooling, Hive test, or vendor dashboard that compares traces byte-for-byte against Geth will fail. The PR ticks Optimization but not Breaking change — this is a silent format regression. The rewritten tests use field-by-field assertion so they cannot detect a future regression back. The field order is forced by the streaming design (the array must be opened before execution), but the breaking-change checkbox must be ticked and the change documented. 2. Block body not found silently returns
|
|
@claude review again. Follow-up: Errors and messages need to be architected properly as separate work - surfacing TransactionResult validation failures, lazy-envelope for pre-flight error parity with Geth, per-scenario Geth-format messages, and differentiated errorCodes. Touches ITxTracer / TransactionProcessor and exception classes used by sequencer/simulate/MEV adapters; scoped to its own ticket. |
|
Claude finished @svlachakis's task in 5m 15s —— View job PR Review: RPC Debug* Streaming Approach — Third PassTasks
What has been fixed since the previous passes ✓
Remaining FindingsHigh1. JSON field order is a silent breaking change — PR checkbox not ticked → inline The streaming path emits: {"structLogs":[…], "gas":…, "failed":…, "returnValue":"0x"}The non-streaming path (and Geth's reference format) emits: {"gas":…, "failed":…, "returnValue":"0x", "structLogs":[…]}The field order is forced by the streaming design ( Medium2.
Fix: store 3.
Simplest fix: add an eager null-guard in 4. No streaming tests for block-trace error paths The new tests cover single-tx cancellation and mid-trace throws. Missing:
Low5. Since the streaming path always resolves to a SummarySecond pass raised 6 High/Medium issues; 5 are now resolved. One High remains (breaking-change checkbox + documentation for field order). Two Mediums remain (missing Recommended before merge:
|
- drop per-tx PipeWriter flush in envelope tracer; StreamingResultBase already flushes once at end and the inner tracer flushes periodically mid-tx, so the envelope-level flush was redundant. - reuse a single GethLikeTxDirectStreamingTracer across all txs in the same block via ResetForNextTx; add a protected ResetTrace() on the base class so the cached GethLikeTxTrace is dropped per tx. - back the storage view with ArrayPoolList<PooledDictionary<UInt256, UInt256>>: outer stack and per-frame slot maps both come from ArrayPool, Clear() keeps the rented arrays alive across pop/push and across txs, Dispose() returns everything to the pool. Eliminates per-frame and per-tx Dictionary allocations. - use DisposableExtensions.DisposeAndNull from #11725 for the block-tracer dispose path.
|
@claude one last review please |
|
Claude finished @svlachakis's task in 6m 26s —— View job PR Review: RPC Debug* Streaming — Final PassTasks
Status of All Prior FindingsAll 17+ High/Medium findings from passes 1–10 remain resolved. No regression from any previously fixed item was found. Architecture Correctness — Re-verified ✓
New FindingsLow — 1:
return new GethLikeTxTraceCollection(tracer.BuildResult());When Low — 2:
private static bool IsAllZero(ReadOnlySpan<byte> slot)
{
for (int i = 0; i < slot.Length; i++) if (slot[i] != 0) return false;
return true;
}
Low — 3 (pre-existing, newly visible): Pre-existing non-streaming path ( FlushResult flushResult = await writer.FlushAsync(cancellationToken);
if (flushResult.IsCompleted || flushResult.IsCanceled) return;When What's GoodThe implementation is correct and well-designed across all paths:
SummaryAll High and Medium findings from passes 1–10 are resolved. Three Low nits remain (discarded empty collection on streaming path, |
- Skip allocating GethLikeTxTraceCollection on the block-streaming path in TraceBlockImpl, where the returned collection is always discarded by the streaming caller. - Replace the scalar IsAllZero loop in GethLikeTxDirectStreamingTracer with ReadOnlySpan<byte>.IndexOfAnyExcept, which picks the SIMD-accelerated BCL implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…esult Wrap the per-trace loop in try/finally so WriteEndArray always runs. Previously, an early return on FlushResult.IsCompleted/IsCanceled (or any exception from the loop body) left the response as a truncated, invalid JSON array. Matches the finally-block pattern used by the new direct streaming envelope writers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 4m 23s —— View job PR Review: RPC Debug* Streaming — Re-review (LukaszRozmej)Tasks
What LukaszRozmej Fixed in This Push
Both the Low nits from pass 9/10 (per-trace loop correctness and scalar-SIMD inconsistency) are resolved. Status of All Prior FindingsAll 17+ High/Medium findings from passes 1–10 remain resolved. No prior finding has regressed. New FindingLow —
|
Match the pattern used by StreamingResultBase.WriteToAsync and GethLikeTxTraceStreamingSingleResult.WriteToAsync: catch OperationCanceledException when the caller's token is cancelled and log at Debug instead of propagating to the HTTP middleware. The inner try/finally still closes the JSON array, so the partial body remains structurally valid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…param Revert the ILogger constructor parameter added in ab855e0 — the outer try/catch in Startup.WriteStreamableResponseAsync already handles OperationCanceledException for all IStreamableResult implementations. Keep the inner try/finally that closes the JSON array so the partial body remains structurally valid before the OCE bubbles up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
High-priority correctness: - StreamingParityLikeTxTracer.OnActionPopped now nulls _trace.Action when the root pops; previously ResetForNextTx/Dispose would walk and re-pool the already-pooled root, handing the same ParityTraceAction out twice on later RentAction calls and silently corrupting traces. - StreamingParityLikeBlockTracer.OnStart disposes the previous reusable tx tracer before replacing it on a per-tx trace-type mismatch (callMany); previously the prior tracer's rented ArrayPool buffers leaked. Cross-PR code reuse with #11693: - Moved StreamingResultBase from Nethermind.JsonRpc.Modules.DebugModule to Nethermind.JsonRpc so both debug_trace* and trace_* streaming results can share the linked-cancellation + Utf8JsonWriter scaffolding. - ParityTxTraceStreamingResult<T> now inherits StreamingResultBase. The outer JSON array / catch(OperationCanceledException) lifecycle lives in the base; subclasses only supply EmitContent. Other: - CappedArrayConverter now routes CappedArray<byte> to a dedicated CappedArrayByteConverter that emits the Ethereum "0x..." hex string, so a future caller that serialises a CappedArray<byte> through JsonSerializer gets the same wire shape as byte[] (the array-of-numbers fallback is kept only for non-byte element types). - StreamingVmFrame.Code is now PooledByteBuffer; ReportByteCode rents from ArrayPool and ReturnFrame eagerly disposes so per-call-frame bytecode no longer allocates a fresh heap array. - OnStart performs ThrowIfCancellationRequested unconditionally (was only inside the Replay-mode branch). - _cachedActionFilter caches the StoreItemPredicate->Func wrapper on the block tracer instead of allocating one per OnStart. - _fillVmTraceSlot / _streamActionsInline / _actionFilter fields hoisted to the top of the file alongside the other readonly state. - IncludedSubtraceCount xmldoc now spells out the divergence from Subtraces.Count under inline streaming mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ess materialisation - Added Hash256-overload constructor on StreamingParityLikeBlockTracer that passes the tx-hash filter through to ParityLikeBlockTracer's ShouldTraceTx hook so trace_replayTransaction / trace_transaction can stream a single envelope from inside a multi-tx block. - Added ParityTxTraceFromReplayStreamingResult, the single-envelope analogue of ParityTxTraceStreamingResult<T> (mirrors GethLikeTxTraceStreamingSingleResult from #11693): inherits ParityTxTraceFromReplay so ResultWrapper<...> stays typed, implements IStreamableResult for the HTTP path, lazily materialises the inherited properties from an optional buffered factory so in-process consumers reading .Data.Action still work without changing every test. - Made ParityTxTraceFromReplay's properties virtual so the streaming variant can override them with lazy materialisation. - trace_replayTransaction now uses the new streaming envelope; trace_transaction uses ParityTxTraceStreamingResult<ParityTxTraceFromStore> via the existing Store-mode infrastructure. - trace_call / trace_rawTransaction stay buffered: state-override scope is tied to the RPC method's lifetime and disposes when the method returns; deferring the trace to serialise-time throws MissingTrieNodeException on the now-invalid overlay. Multi-tx streaming already covers the heap savings and single-tx working sets are tiny. Tests: - Context.Build(... bool enableStreaming = true) plumbs the flag via WithConfig + DI singleton registration of IJsonRpcConfig. - Representative tests for the newly-streaming methods are parameterised with [TestCase(true)] [TestCase(false)] so both paths get coverage: Trace_replayTransaction_test, Trace_replayTransaction_reward_test, Trace_callMany_is_blockParameter_optional_test, Trace_callMany_accumulates_state_changes. - AssertJsonEquivalent helper (JToken.DeepEquals) replaces literal string equality where the streaming path emits vmTrace first and the buffered path emits it last; one expected JSON works for both modes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Name / Identifier Stavros Vlachakis Team / Project Nethermind Start date of relevant projects July 2025 (part-time) February 2026 (full-time) Proposed weight Full (1.0) Summary of work / eligibility Stavros joined Nethermind in March 2025. Since July 2025, he has contributed part-time to the Nethermind Ethereum Execution Client (Core team) alongside other responsibilities. Since February 2026, he has worked full-time on Nethermind Client, with 100+ merged PRs in total. He owns the JSON-RPC for Nethermind Client. He is also expected to contribute extensively to Frame Transactions on Hegota. Representative work: - EIP-4444 history expiry (EraE): implemented the EraE archive format end-to-end — era export/import and remote download with SHA-256 verification. (#10812 (NethermindEth/nethermind#10812)) - JSON-RPC (owner): broad spec-compliance and Geth-parity work plus new endpoints — e.g. Geth-compatible error codes (#11335 (NethermindEth/nethermind#11335)) and eth_signTransaction / raw-transaction methods (#11517 (NethermindEth/nethermind#11517), #11521 (NethermindEth/nethermind#11521)). - Streaming for large RPC responses: streaming approach for trace_* and debug_* results, avoiding buffering huge responses in memory. (#11755 (NethermindEth/nethermind#11755), #11693 (NethermindEth/nethermind#11693)). - EVM & execution performance: eth_call interpreter/dispatch optimizations (#11965 (NethermindEth/nethermind#11965)), EVM memory pooling and SLOAD / flat-state read caching (#11991 (NethermindEth/nethermind#11991), #12043 (NethermindEth/nethermind#12043)). - State & consensus reliability: correct persisted-code tracking in the code DB (#11714 (NethermindEth/nethermind#11714)), forkchoice canonical-chain corruption healing after beacon sync (#10876 (NethermindEth/nethermind#10876)). All merged PRs: https://github.com/NethermindEth/nethermind/pulls?q=is%3Apr+author%3Asvlachakis+is%3Aclosed (edited)
Fixes Closes Resolves #11718
Overview
Default-tracer struct-log traces are now streamed per-opcode through:
IStreamableResult → Utf8JsonWriter → PipeWriterinstead of being accumulated into a
List<GethTxTraceEntry>and serialized at the end.Peak memory is now
O(1)entries regardless of trace length, and time-to-first-byte drops from “after trace completes” to “first opcode boundary”.Throughput (single trace, single thread)
PeakHeap (16 concurrent traces, discarding sink)
Real-node benchmark
Block: 25,151,364 (0x17fc784) — 59.8 M gas, 498 txs
Output size: 2,092,406,085 bytes (2.09 GB) — identical across modes
Summary
Raw runs
Summary
Concurrency Benchmark (8 cores VM)
8 simultaneous ~1000 MB traces = ~7–8 GB of JSON flowing concurrently for ~10 seconds straight, with no OOM, no thread starvation, all returning byte-identical complete responses.
Changes
Scope
Streaming activates only when
options.Traceris null/empty (default struct-log tracer).Custom tracers still use the existing buffered path:
callTracerprestateTracer4byteTracerStreamed Methods
debug_traceTransactiondebug_traceCalldebug_traceTransactionByBlockAndIndexdebug_traceTransactionByBlockhashAndIndexdebug_traceTransactionInBlockByHashdebug_traceTransactionInBlockByIndexdebug_traceBlockdebug_traceBlockByNumberdebug_traceBlockByHashdebug_traceCallManyBehavior Changes
JSON field order for the default struct-log tracer: streamed responses emit
structLogsfirst, thengas/failed/returnValue. The non-streaming (custom-tracer) path and Geth's reference implementation emitgas/failed/returnValue/structLogs. The order is forced by the streaming design — the array must be opened before EVM execution begins so entries can be emitted per opcode. Clients that parse by field name are unaffected; byte-for-byte comparators against Geth (Hive conformance, vendor dashboards) will see a diff.Pre-flight failures on
debug_traceCall/debug_traceCallMany: previously returned a JSON-RPCerrorenvelope; now return aresultenvelope withfailed: true,error: "tracing failed: …",errorCode: -32000. On-chain trace methods (debug_traceTransaction*,debug_traceBlock*) are unaffected — their txs already passed validation at block-creation time.Partial
structLogson mid-execution failure: failing traces may now contain the N opcode entries that executed before the failure, instead of an empty array. Inherent to streaming.Follow-up — error/message architecture
Geth-compat parity on error semantics is scoped as separate work, not bundled here:
TransactionResultvalidation failures (nonce, intrinsic gas, EIP-3607, etc.) through to the tracer — fixes a pre-existing silent-failure gap that affects buffered too.errorenvelope instead of mid-streamfailed: true."insufficient funds for gas * price + value: address X have N want M", etc.).errorCodes beyond the currentInvalidInput/InternalErrorsplit.These touch
ITxTracer/TransactionProcessorand exception classes used by sequencer/simulate/MEV adapters.Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?