Skip to content

fix(evm-rpc): accept Scroll's empty call frame for a failed L1 message - #603

Merged
abernatskiy merged 5 commits into
masterfrom
fix/evm-rpc-empty-debug-frame
Oct 22, 2026
Merged

abernatskiy merged 5 commits into
masterfrom
fix/evm-rpc-empty-debug-frame

Conversation

@abernatskiy

Copy link
Copy Markdown
Contributor

Problem

Scroll's l2geth call tracer answers {} for an L1 message (type 0x7e) that fails before execution. Example: scroll-mainnet block 35441201, tx 0xe398628cd2ea5c83d50b0e46e729caaf436f17a1adb3ddfb3a04837cc1f4dab5, a self-transfer with empty input, status 0x0, gasUsed 0x5208. Every provider we asked gives the same answer:

[{"txHash": "0xe398628c…f4dab5", "result": {}}]

DebugFrame requires type, from and gas, so the whole debug_traceBlockBy* response fails with invalid value at /0/result/type: undefined is not a string. Retrying can't help, so anything that fetches Scroll traces stops on this block. Older L1 messages that executed still trace normally.

Change

  • An empty frame ({}) is now read as "no call frame" rather than a schema error. This covers both the block-level call and the per-transaction fallback used for oversized responses.
  • ChainUtils.allowsEmptyCallFrame(tx) decides where that's legitimate. For now that's only Scroll (mainnet and Sepolia) L1 messages. The transaction is kept with no traces; normalization already skips missing frames.
  • Any other empty frame rejects the block with empty debug call frame for transaction <hash>. That's no looser than before, and the message now names the transaction.

Tests

  • rpc.empty-debug-frame.test.ts, using an unedited fixture of the Scroll block and its trace response. It covers three cases:

    • a Scroll L1 message is accepted;
    • a Scroll transaction of another type with an empty frame is rejected;
    • an empty frame on Sepolia is rejected.

    All three fail without the fix.

  • verification.test.ts runs over every fixture, so it now skips transactionsRoot for Scroll. Scroll's 0x7e encoding (not an OP deposit) isn't implemented; that limitation already existed.

  • rushx test in evm/evm-rpc: 470 passed, 30 skipped. rush build --from @subsquid/evm-rpc passes.

🤖 Generated with Claude Code

Scroll's l2geth answers `{}` from the call tracer for an L1 message
(type 0x7e) that fails before execution, e.g. scroll-mainnet tx
0xe398628c...f4dab5 in block 35441201. The frame schema requires
`type`, `from` and `gas`, so the whole debug_traceBlockBy* response
failed validation and every consumer stopped on that block.

An empty frame is now read as "no call frame". On Scroll it is
accepted for an L1 message, and that transaction is stored with no
traces. Anywhere else the block is rejected with "empty debug call
frame for transaction <hash>", instead of a schema error that names
no transaction. The per-transaction fallback for oversized responses
gets the same handling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

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.

🟡 Changes recommended

Trace-only requests still reject legitimate Scroll empty frames, and discarded frames bypass transaction-hash validation.

2 open issues
What changed in this PR

Handles Scroll’s empty call-tracer responses for L1 messages that fail before execution, allowing affected blocks to be processed.

Changes:

  • Recognizes empty frames in block and per-transaction tracing.
  • Adds Scroll-specific eligibility checks and regression fixtures.
  • Skips unsupported Scroll transaction-root verification in fixture tests.
File Description
evm/​evm-rpc/​test/​verification.test.ts Skips Scroll transaction-root checks.
evm/​evm-rpc/​test/​rpc.empty-debug-frame.test.ts Tests empty-frame acceptance and rejection.
evm/​evm-rpc/​test/​helpers/​fixture-loader.ts Registers Scroll’s chain ID.
evm/​evm-rpc/​test/​fixtures/​scroll/​35441201/​debug-frames.json Captures the empty trace response.
evm/​evm-rpc/​test/​fixtures/​scroll/​35441201/​block.json Adds the affected block fixture.
evm/​evm-rpc/​src/​rpc.ts Handles and classifies empty frames.
evm/​evm-rpc/​src/​chain-utils.ts Defines Scroll empty-frame eligibility.
common/​changes/​@subsquid/​evm-rpc/​fix-evm-rpc-empty-debug-frame_2026-10-21-14-20.json Records the patch release change.

Review effort: Balanced

Comment thread evm/evm-rpc/src/chain-utils.ts
Comment thread evm/evm-rpc/src/rpc.ts
… their label

On a chain whose tracer may answer `{}` (Scroll), a traces request
without full transactions now fetches transaction objects internally,
because only the type tells a legitimate empty frame from a broken
one. The caller still gets hashes back.

An empty frame labelled with another transaction now rejects the
block with the same message validateDebugFrames() uses, instead of
being dropped before that check runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

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.

🟡 Changes recommended

Address the unresolved transaction classification, frame-label validation, fallback-path coverage, and verification-scope issues.

2 open issues
2 resolved since last review

Review effort: Lite

Comment thread evm/evm-rpc/src/rpc.ts
Comment thread evm/evm-rpc/test/verification.test.ts Outdated
Oversized block-level trace responses fall back to debug_traceTransaction,
which judges empty frames on its own; test Scroll acceptance and
non-Scroll rejection there too.

Skip the transactionsRoot check only for Scroll blocks that carry a 0x7e
L1 message, not for every Scroll fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

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.

🟡 Changes recommended

Scroll trace requests with transaction-root verification can still fail because the unsupported type 0x7e transaction is forced through verification.

1 open issue
2 resolved since last review

Review effort: Lite

Comment thread evm/evm-rpc/src/rpc.ts
…ation

Type 0x7e was always encoded as an OP deposit, so verifyTxRoot threw on
any Scroll block with an L1 message (no sourceHash). Since Scroll
traces now fetch full transactions even for hash-only requests, that
also hit hash-only trace requests with root verification on.

Encode a 0x7e transaction that carries queueIndex as l2geth's
L1MessageTx: 0x7e || rlp([queueIndex, gas, to, value, data, sender]).
It reproduces the root of scroll-mainnet block 35441201, so the
verification test no longer skips Scroll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

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.

🟡 Changes recommended

Eligibility must be restricted to the intended Scroll L1 message shape, and wire-level JSON null frames must remain rejected.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

Medium severity Reject null debug frames in block trace responses

evm/​evm-rpc/​src/​rpc.ts:1606

nullable(DebugFrame) accepts an actual JSON null from debug_traceBlockByNumber, and dropEmptyDebugFrames() treats it exactly like the internal null sentinel produced for {}. Before this change DebugFrame rejected null (the per-transaction fallback still does), so the block-level path now accepts a malformed response that is not the documented empty object. Keep the wire validator non-null and map only isEmptyFrame(result) to the internal missing-frame representation.

1 resolved since last review

Review effort: Lite

Comment thread evm/evm-rpc/src/chain-utils.ts Outdated
…r L1 messages

The block-level frame validator used nullable(DebugFrame), so a JSON
`null` frame was treated like the `{}` an L1 message legitimately
gets. Validate the non-empty frames with the strict schema and carry
only real `{}` frames forward as `result: null`.

Classify a Scroll L1 message by type 0x7e plus queueIndex, the same
shape the transactions-root encoder uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abernatskiy

Copy link
Copy Markdown
Contributor Author

(With Claude): About the review-body finding "Reject null debug frames in block trace responses": agreed, fixed in bcd238e. The block-level path validates the non-empty frames with the strict DebugFrameResult schema again. Only frames that are exactly {} go forward as result: null (their txHash is still validated), so a JSON null frame fails validation as it did before this PR. New test: keeps a JSON null call frame a validation error (fails without the fix).

Copilot AI left a comment

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.

🟢 Approval recommended

The only noted issue is a non-blocking documentation nit; no unresolved code issues block approval.

No open issues 🎉

1 resolved since last review

Review effort: Lite

@abernatskiy
abernatskiy merged commit 2186673 into master Oct 22, 2026
4 of 5 checks passed
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.

2 participants