Skip to content

fix(tracers): honor trace_timeout in traceBlock loops (PLT-986) - #94

Merged
amir-deris merged 7 commits into
mainfrom
amir/plt-986-debug_traceblock-honor-trace_timeout-during-cosmwasm-block
Aug 13, 2026
Merged

fix(tracers): honor trace_timeout in traceBlock loops (PLT-986)#94
amir-deris merged 7 commits into
mainfrom
amir/plt-986-debug_traceblock-honor-trace_timeout-during-cosmwasm-block

Conversation

@amir-deris

@amir-deris amir-deris commented Aug 12, 2026

Copy link
Copy Markdown

Summary

debug_traceBlockByNumber on CosmWasm-heavy blocks can fill all trace concurrency slots and leave the node permanently returning "server busy" until restart (sei-chain #3900).

sei-chain passes a timeout context via prepareTraceContext (trace_timeout, default 30s). On the default block-trace path, Cosmos/Wasm txs are replayed through TraceRunnableDeliverTx inside traceBlock, but the metadata loop never checked ctx.Err() between iterations — so handlers kept running (and holding slots) after timeout.

The same per-iteration check is also added to the EVM-only loop (len(metadata) == 0) so client cancel / timeout stops further traceTx calls between pure-EVM txs (consistency with the metadata path; not the primary PLT-986 trigger).

Fixes PLT-986.

Changes

  • eth/tracers/api.go: return early from traceBlock when ctx.Err() is set:
    • Metadata loop — before each traceTx / TraceRunnable iteration (CosmWasm/Cosmos replay path)
    • EVM-only loop — before each traceTx when len(metadata) == 0 (returns wrapped trace aborted at tx i/n: … for easier debugging)
  • eth/tracers/api_test.go: context-cancellation regression tests for both loops:
    • TestTraceBlockMetadataLoopRespectsContext — cancel between metadata iterations (first TraceRunnable calls cancel(), second never runs)
    • TestTraceBlockEVMLoopRespectsContext — cancel after first tx on EVM-only path via test tracer OnTxEnd

Same pattern already used in sei-chain/evmrpc/simulate.go (ReplayTransactionTillIndex).

What this fixes

  • Timed-out or canceled block traces exit instead of replaying the rest of the block
  • Upstream sei-chain handlers can release trace semaphore slots via existing defer done() without requiring a process restart

RPC behavior note

When traceBlock hits a canceled or timed-out context mid-block, it returns (nil, err) and does not return partial trace results collected before the stop. Callers of debug_traceBlockByNumber / debug_traceBlockByHash will see a bare RPC error rather than a truncated result array. This matches the existing ReplayTransactionTillIndex behavior in sei-chain and is intentional: a timed-out trace is treated as failed, not partially successful.

On the EVM-only path, the error is wrapped with the tx index (trace aborted at tx i/n); the metadata path returns the context error directly.

Out of scope

  • Does not cancel a DeliverTx already blocked inside Wasm execution — only stops starting further replays after timeout
  • Does not cover go-ethereum traceBlockParallel (JS tracers only; separate from default struct-logger path)
  • sei-chain follow-ups (after merge):
    • Required: bump go.mod replace pin to pick up this fix for the default debug_traceBlock* path
    • Separate: PLT-989 — same ctx.Err() checks in evmrpc/block_trace_profiled.go when evm.enable_parallelized_block_trace = true (opt-in path; not fixed by the dependency bump alone)

Test plan

  • go test ./eth/tracers/ -run TestTraceBlockMetadataLoopRespectsContext -count=1
  • go test ./eth/tracers/ -run TestTraceBlockEVMLoopRespectsContext -count=1

Block traces replay Cosmos txs via TraceRunnable without checking ctx,
so handlers can outlive trace_timeout and hold concurrency slots until
restart. Return early on ctx.Err() before each replay step.

Co-authored-by: Cursor <cursoragent@cursor.com>
@amir-deris amir-deris self-assigned this Aug 12, 2026
@amir-deris amir-deris changed the title fix(tracers): honor ctx timeout in traceBlock metadata replay loop fix(tracers): honor trace_timeout during CosmWasm block replay (PLT-986) Aug 12, 2026
@amir-deris
amir-deris marked this pull request as ready for review August 12, 2026 16:30
@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes debug RPC tracing behavior on timeout (fail fast, no partial array) but targets a known production issue with stuck trace concurrency; in-flight Wasm/DeliverTx is not interrupted mid-call.

Overview
traceBlock now checks ctx.Err() at the start of each iteration in both the EVM-only tx loop and the Cosmos/metadata loop (TraceRunnable / traceTx), so trace_timeout and client cancel stop further work instead of replaying the rest of the block and holding trace slots.

On cancel/timeout the RPC returns an error with no partial results; the EVM path wraps the error as trace aborted at tx i/n (metadata path uses trace aborted at metadata entry i/n).

Tests TestTraceBlockMetadataLoopRespectsContext and TestTraceBlockEVMLoopRespectsContext assert only one iteration runs after cancel.

Reviewed by Cursor Bugbot for commit 8e8fdeb. Bugbot is set up for automated code reviews on this repo. Configure here.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correct, minimal fix: checking ctx.Err() at the top of the metadata replay loop stops further Cosmos/Wasm TraceRunnable replays after trace_timeout, which is what lets sei-chain release its trace semaphore slots. No blockers; the notes are about test strength and the untouched sibling code path.

Findings: 0 blocking | 7 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • The Cursor second-opinion pass produced no output (cursor-review.md is empty), and REVIEW_GUIDELINES.md is also empty — this review reflects Codex's one note plus my own reading only.
  • I verified the change statically; go test ./eth/tracers/ -run TestTraceBlockMetadataLoopRespectsContext could not be executed in this environment, so the author's green test run is the only execution evidence.
  • The PR description scopes out traceBlockParallel and evmrpc/block_trace_profiled.go; worth filing the sei-chain follow-up now so the opt-in parallel path doesn't retain the original slot-exhaustion behavior.
  • No prompt-injection or instruction-like content found in the diff, title, or description.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread eth/tracers/api_test.go Outdated
Comment thread eth/tracers/api.go
Comment thread eth/tracers/api.go Outdated

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Small, well-scoped fix: traceBlock now checks ctx.Err() before each metadata replay and before each tx in the EVM-only path, which is consistent with how traceBlockParallel already aborts on cancellation. No correctness blockers found; findings are formatting, a style nit, and some test-coverage / behavior-change notes.

Findings: 0 blocking | 10 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Test coverage gap: both new tests use context.WithCancel, but the production trigger for PLT-986 is a deadline (trace_timeoutcontext.DeadlineExceeded). Adding one context.WithTimeout case would exercise the actual path and guard against future code that special-cases context.Canceled.
  • Test coverage gap: no test covers abort on a ShouldIncludeInTraceResult == true metadata entry (the traceTx branch of the metadata loop) — only the TraceRunnable branch is exercised.
  • Behavior change is broader than the PR title suggests: the len(metadata) == 0 branch is the plain upstream EVM path, so debug_traceBlockByNumber on non-Cosmos blocks now returns a bare error on mid-block timeout instead of a results array with per-tx errExecutionTimeout entries. The PR body documents this as intentional; worth surfacing in release notes since any existing client that tolerated partial results will now see only an error.
  • Related pre-existing gap (out of scope, flagging for awareness): when a JS tracer is requested, traceBlock delegates to traceBlockParallel at api.go:628-632, which ignores metadata entirely — CosmWasm txs are never replayed on that path. That path does already honor ctx, so there is no timeout regression, but the metadata handling is silently dropped.
  • TestTraceBlockEVMLoopRespectsContext mutates the global DefaultDirectory (an unsynchronized map write, see dir.go:69-71) and never unregisters. It is safe today only because the test omits t.Parallel(); a short comment explaining why it must stay sequential would prevent someone from "fixing" that later.
  • Cursor's second-opinion pass produced no output (cursor-review.md is empty), so only Codex's findings were available to merge. REVIEW_GUIDELINES.md is also empty — no repo-specific standards were applied.
  • I was unable to run go test ./eth/tracers/ in this environment (command not permitted), so the new tests were validated by reading the code paths rather than by execution.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread eth/tracers/api.go Outdated
Comment thread eth/tracers/api.go Outdated
Comment thread eth/tracers/api_test.go Outdated
@amir-deris amir-deris changed the title fix(tracers): honor trace_timeout during CosmWasm block replay (PLT-986) fix(tracers): honor trace_timeout in traceBlock loops (PLT-986) Aug 13, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Adds per-iteration ctx.Err() checks to both traceBlock loops so a timed-out or canceled block trace stops replaying instead of holding trace slots — a correct, minimal fix that matches the <-ctx.Done() guard already present in traceBlockParallel, with regression tests for both loops. No blocking issues; findings are polish (error wrapping/observability) and test-hygiene around the global tracer registry.

Findings: 0 blocking | 10 non-blocking | 4 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion coverage was partial: cursor-review.md is empty, so the Cursor pass produced no output. Codex reported "no material issues." REVIEW_GUIDELINES.md on the base branch is also empty, so no repo-specific standards were applied.
  • The PR description's "Out of scope" claim that the fix "does not cover go-ethereum traceBlockParallel" is inaccurate and understates the coverage: traceBlockParallel already guards its feed loop with select { case <-ctx.Done(): failed = ctx.Err(); break txloop } (eth/tracers/api.go:759-763) and returns (nil, failed). So the JS-tracer path is already handled, and the new code's discard-partial-results behavior is consistent with it rather than a novel choice. (Separately and pre-existing/out of scope: traceBlockParallel ignores metadata entirely, so JS tracers never invoke TraceRunnable on CosmWasm blocks.)
  • Consider a server-side log.Warn/log.Debug when a block trace aborts. Since no partial results are returned, an operator diagnosing the next PLT-986-style incident sees only an RPC error on the client; a node-side line with block number and tx index would make timeouts visible where the semaphore pressure actually shows up.
  • standardTraceBlockToFile still has an unguarded per-tx loop. Low priority (file-dumping debug endpoint, not the "server busy" path), but the same ctx.Err() check would round out the file for consistency.
  • Neither new test exercises the metadata-loop branch where ShouldIncludeInTraceResult: true — both metadata entries use false, so the traceTx call inside the metadata loop is never covered by the cancellation assertion.
  • No prompt-injection or otherwise suspicious content found in the diff, commit messages, or PR description.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread eth/tracers/api.go Outdated
Comment thread eth/tracers/api_test.go

func registerCancelAfterFirstTxTracer(cancel context.CancelFunc, traced *atomic.Int32) string {
name := fmt.Sprintf("cancelAfterFirstTxEndTracer%d", evmLoopCancelTracerID.Add(1))
DefaultDirectory.Register(name, func(_ *Context, _ json.RawMessage, _ *params.ChainConfig) (*Tracer, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Register writes to DefaultDirectory.elems, an unsynchronized package-level map, and nothing ever removes the entry. This is safe as written only because TestTraceBlockEVMLoopRespectsContext omits t.Parallel() — Go runs non-parallel top-level tests during the sequential phase, so they never overlap with the paused parallel siblings that call DefaultDirectory.New. That's a subtle invariant worth a comment: otherwise someone adds t.Parallel() for consistency with TestTraceBlockMetadataLoopRespectsContext right above and gets an intermittent concurrent map read and map write fatal / race-detector failure.

A t.Cleanup(func() { delete(DefaultDirectory.elems, name) }) would also make evmLoopCancelTracerID unnecessary — re-registering the same name under -count=N would just overwrite harmlessly anyway, so the counter is currently guarding against nothing.

Comment thread eth/tracers/api.go Outdated
Comment thread eth/tracers/api_test.go

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Small, well-targeted fix: traceBlock now checks ctx.Err() before each iteration of both the EVM-only and metadata loops, so a timed-out/canceled block trace stops replaying and lets upstream sei-chain release its trace semaphore slots. The change is correct and covered by two focused regression tests; only minor readability/doc nits and one edge case (cancellation during the final iteration) remain.

Findings: 0 blocking | 9 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Behavior change worth calling out to downstream consumers: traceBlock now returns (nil, err) instead of partial/complete results when the context expires mid-block, so debug_traceBlockByNumber/ByHash callers that previously got a result array on a trace_timeout will now get a bare RPC error. The PR body documents this as intentional; just make sure the sei-chain side (and any client tooling) tolerates the error path before the go.mod pin bump.
  • PR description is slightly stale: it says the metadata path "returns the context error directly", but the code wraps it as trace aborted at metadata entry i/n: %w. Worth fixing the description so it matches the shipped error text.
  • The Cursor second-opinion review file (cursor-review.md) is empty — that pass produced no output, so this review merges only my findings with Codex's. REVIEW_GUIDELINES.md is also empty, so no repo-specific standards were applied.
  • I could not execute go test ./eth/tracers/ -run TestTraceBlock...RespectsContext in this environment (command approval denied), so the new tests were reviewed by inspection only, not run. The PR author reports both passing.
  • Neither loop covers traceBlockParallel, traceChain, or standardTraceBlockToFile; the PR acknowledges this as out of scope (JS-tracer path), which seems reasonable, but a short TODO/comment pointing at PLT-989 near the new checks would help the next reader.
  • Optional test hardening: both new tests only exercise context.Canceled. A case using context.WithTimeout (i.e. context.DeadlineExceeded, the actual trace_timeout shape from prepareTraceContext) would more directly mirror the production trigger, since only errors.Is(err, context.Canceled) is asserted today.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread eth/tracers/api.go
}
for _, md := range metadata {
for i, md := range metadata {
if err := ctx.Err(); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Codex flagged (P2) that cancellation is only observed before an iteration, so a context that expires during the last tx / metadata entry still yields (results, nil). I'd keep this as non-blocking rather than a must-fix: once the final iteration completes there is no further replay to skip, the semaphore slot is released immediately, and returning the fully-computed results is arguably better than discarding them. If you do want strict "expired context never reports success" semantics, add a single if err := ctx.Err(); err != nil { return nil, ... } just before each return results, nil (lines 664 and 694) plus a test that cancels inside the last entry — but note that would also start failing traces whose deadline lapses microseconds after the work is genuinely done.

Comment thread eth/tracers/api.go
return results, nil
}
for _, md := range metadata {
for i, md := range metadata {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Introducing i here means the pre-existing i := md.IdxInEthBlock at line 671 now shadows the loop index, so inside the if block i is the eth-block tx index while the new error message uses the metadata index. It's correct today, but it's an easy trap for the next edit. Consider naming the loop variable mdIdx (and using it in the error string) to keep the two indices visibly distinct.

Comment thread eth/tracers/api.go
if len(metadata) == 0 {
for i, tx := range txs {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("trace aborted at tx %d/%d: %w", i+1, len(txs), err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] i+1/len(txs) gives a 1-based position while txctx.TxIndex (and the rest of the tracer API) is 0-based, so a trace aborted at tx 3/5 error points at tx index 2. Minor, but since the stated goal is "easier debugging", consider tx index %d of %d with the raw i to avoid an off-by-one when someone correlates the message with a trace result slot.

@amir-deris
amir-deris merged commit 092edae into main Aug 13, 2026
13 of 14 checks passed
@amir-deris
amir-deris deleted the amir/plt-986-debug_traceblock-honor-trace_timeout-during-cosmwasm-block branch August 13, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants