Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🟡 Changes recommended
Exact stack-size validation can mask timeout errors and conflicts with the documented unbalanced-stack behavior.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes native tracers to report real EVM frames when available and synthetic frames for routed non-EVM transactions.
Changes:
- Corrects frame, gas, and log selection.
- Adds synthetic-frame support to flat traces.
- Adds unit and routing regression tests.
File summaries
| File | Description |
|---|---|
| eth/tracers/native/call.go | Selects and updates the correct frame. |
| eth/tracers/native/call_test.go | Tests preservation of real frames. |
| eth/tracers/native/call_flat.go | Adds synthetic flat-frame handling. |
| eth/tracers/native/call_flat_test.go | Tests flat tracer frame selection. |
| eth/tracers/internal/tracetest/non_evm_trace_test.go | Tests production transaction routing. |
Review details
Suppressed comments (1)
eth/tracers/internal/tracetest/non_evm_trace_test.go:72
- Handle the key-generation error before dereferencing
key; an entropy-source failure currently turns a clear setup error into a nil-pointer panic.
key, _ := crypto.GenerateKey()
from := crypto.PubkeyToAddress(key.PublicKey)
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…address tx The call tracer flags a transaction to one of the XDCX system addresses as non-EVM from the to address alone, and used to return the synthetic frame it prepares on OnTxStart whenever that flag was set. The flag does not know whether the fork that routes those transactions away from the EVM is active: before the fork, and after the receiver-disable fork, the EVM executes them and pushes a real top-level frame. That frame was silently replaced by the synthetic one, dropping its output, error and sub-calls, so debug_traceTransaction returned degraded data for those transactions — while the flat tracer kept using the real frame, so the two tracers disagreed on the very same transaction. Prefer the real top-level frame whenever one was pushed and let the synthetic frame be the fallback for transactions the EVM never entered. OnTxEnd and OnLog follow the same decision. Pre-existing and XDPoS-specific: geth has no non-EVM transaction concept, so there is no upstream fix to port. Add TestCallTracerKeepsRealFrame, and keep TestCallTracerNonEVMTx covering the synthetic frame of a transaction the EVM did not execute.
4c5032e to
d44a344
Compare
flatCallTracer built its answer from the callstack of the call tracer and failed the whole trace with "invalid number of calls" when that callstack was empty. It is empty exactly when the EVM did not execute the transaction, which is what block processing does to the transactions it routes away from the EVM: those to the XDCX system addresses while the receiver fork is active, and the block-signing transactions. debug_traceBlock* therefore failed the whole call on any block carrying one of them. Flatten the synthetic frame the call tracer prepares for those transactions when no real frame was pushed, and keep the real frame whenever the EVM did run, so the flat tracer reports the same top-level frame as the call tracer. Add TestFlatCallTracerNonEVMTx, TestFlatCallTracerKeepsRealFrame and TestFlatCallTracerUnbalancedCallstack for the flat tracer itself, and TestNonEVMTxThroughBlockProcessingRouting, which traces a transaction to a system address through core.ApplyTransactionWithEVM and checks both tracers against the receipt. No corresponding fix exists upstream: geth has no non-EVM transaction concept.
d44a344 to
5c429f9
Compare
Proposed changes
The call tracer decides that a transaction is non-EVM from its
toaddress alone (types.Transaction.IsNonEVMTx(): the system addresses0x91/0x92/0x93/0x94and the block signers address0x89), and returned the synthetic frame it prepares onOnTxStartwhenever that flag was set. The flag does not know whether the fork that routes those transactions away from the EVM is active: beforeTIPXDCXBlockand afterTIPXDCXReceiverDisableBlock, block processing executes them as ordinary EVM transactions and the EVM pushes a real top-level frame.OnTxEndandOnLogfollowed the flag as well, so the receipt gas was written into the synthetic frame and the real one kept 0, andGetResultshadowed the real frame with the synthetic one.flatCallTracerbuilds its answer from that same callstack and failedGetResultwithinvalid number of callswhen the callstack was empty — which is exactly the state of a transaction the EVM never entered: transactions to the system addresses while the receiver fork is active, and every block signer transaction (0x89).Symptoms
debug_traceBlockByNumber/ByHash/traceBlockwithflatCallTracerfails the whole call withinvalid number of callsfor any block carrying a block signer transaction (0x89) — those are not skipped byIsSkipNonceTransaction(), so they do reach the tracer. Measured on mainnet block 37,849,456 (0x2418970) and Apothem block 48,667,667 (0x2e69c13).flatCallTracerreportsgasUsed: 0x0for a transaction to a system address that the EVM did execute (receiver fork window not active), because the receipt gas went into the synthetic frame, which that tracer does not read. Measured on mainnet block 37,849,457 (0x2418971) transaction 0:gasUsed0x0against a receipt of0x6308.callTracerreturns the synthetic frame whenever the flag is set, even when the EVM pushed a real top-level frame, sodebug_traceTransaction,debug_traceCallanddebug_traceBlock*disagree withflatCallTraceron the same transaction. On the live networks the two frames happen to coincide today — the five system addresses hold no code (eth_getCodeis0xfor0x89and0x91…0x94, at block0x2418971and atlatest), so a transaction they execute produces no output, no error and no sub-calls — and the difference shows as soon as an execution produces any of those.TestCallTracerKeepsRealFramepins the choice of frame.Fix
Two commits.
fix(eth/tracers): the call tracer prefers the real top-level frame whenever the EVM pushed one, and keeps the synthetic frame only as the fallback for transactions the EVM never entered.OnTxEndandOnLogfollow the same decision, so the real frame carries the receipt gas, andGetResultstops shadowing it.fix(eth/tracers):flatCallTracerflattens the synthetic frame the call tracer prepares when no real frame was pushed, instead of failing the whole trace withinvalid number of calls; when a real frame exists it keeps that one, with the gas the first commit gives it. An interrupted trace reports its interruption reason rather than an internal error: an unbalanced callstack is flattened from its first frame, as the baseline and upstream do.The two are one PR because they are the two exits of the same decision: the flat tracer reads the real frame, and only commit 1 makes that frame carry the receipt gas.
Upstream
No upstream fix to port: geth has no non-EVM transaction concept, so neither the synthetic frame nor the unconditional flag has a geth counterpart.
Tests
eth/tracers/native/call_test.go—TestCallTracerKeepsRealFrame: a system-address transaction the EVM did execute keeps its real top-level frame, with its gas, output and logs; the base implementation returns the synthetic frame, whose output is empty.eth/tracers/native/call_test.go—TestCallTracerNonEVMTx: kept, and still covers the synthetic frame of a transaction the EVM never entered.eth/tracers/native/call_flat_test.go—TestFlatCallTracerNonEVMTx: the flat tracer flattens the synthetic frame instead of failing on the empty callstack.eth/tracers/native/call_flat_test.go—TestFlatCallTracerKeepsRealFrame: the flat tracer uses the real frame, with the receipt gas, when the EVM pushed one; fails without the call tracer commit of this PR.eth/tracers/native/call_flat_test.go—TestFlatCallTracerUnbalancedCallstack: aStopin the middle of a nested call leaves two frames on the callstack; the root frame is still reported, together with the interruption reason, and the frame left open by the interruption is dropped, as upstream does.eth/tracers/internal/tracetest/non_evm_trace_test.go—TestNonEVMTxThroughBlockProcessingRouting: runsflatCallTracerandcallTracerover both receiver fork settings through the production routing (core.ApplyTransactionWithEVM, hooked state, real EVM) and checks the single frame — type, sub-traces,toandgasUsed— against the receipt.End-to-end verification
Four binaries were run one after the other on the same node and the same data directory:
baseline=dev-upgrade@cdce8fc5c,commit 1=7adea3967,this branch=5c429f99a, andearlier=4c5032e5d, the tip this branch had before the interruption fix, kept only for the last table. The first three tables were measured on mainnet (~/xdc_chain/mainnet_2, RPC 8646); the Apothem table was measured on its own data directory (~/xdc_chain/testnet_1, RPC 8745) with4c5032e5das the branch tip, because the probes it contains never enter the interruption path that5c429f99achanges. The RPCs were called withdebugexposed (--rpcapi/RPC_APIincludesdebugin theLocal_DPoS_Setupconfigs).Mainnet block 37,849,456 (
0x2418970, 4 transactions; sign transactions at index 0 and 2, no system-address transaction, receiver fork window not active):debug_traceBlockByNumber(flatCallTracer)invalid number of calls, the whole call failsinvalid number of callsnullsdebug_traceBlockByNumber(callTracer)Mainnet block 37,849,457 (
0x2418971, 204 transactions; transaction 0 to0x92, which the EVM executes because the receiver fork window is not active; transaction 95 to0x90from the same sender, nonce +1):debug_traceTransaction(transaction 0, flatCallTracer)gasUsed0x0gasUsed0x6308, the receipt gasdebug_traceTransaction(transaction 0, callTracer,withLog)output,errororcalls— the same fields the real frame has for this transactiondebug_traceBlockByNumber(flatCallTracer)invalid number of calls, it fails on the first sign transactioninvalid number of callstracing failed: nonce too high: address xdcE3bC38418f89C386d1093aee89d606564012e329, tx: 1547093 state: 1547092— the trace now walks past the ~100 sign transactions of the block and fails later on the pre-fork skip, which is #2581debug_traceBlockByNumber(callTracer)tracing failed: nonce too high(the same skip)Apothem block 48,667,667 (
0x2e69c13, 8 transactions; transaction 0 to0x92, transactions 1–7 sign transactions, receiver fork window active):4c5032e5ddebug_traceBlockByNumber(flatCallTracer)invalid number of calls, the whole call failsnull(transaction 0, the system-address transaction is still skipped, that side is #2581)debug_traceBlockByNumber(callTracer)nullnullto0x89,gasUsed0x0Interruption inside a nested call — the fix
5c429f99acarries on top of4c5032e5d:debug_traceTransactionwithflatCallTracerand smalltimeoutvalues (1, 2, 5 and 20 ms, three runs each) on a deep transaction of mainnet block0x6626778(13 frames, 5 levels deep, 160,494 gas):cdce8fc5cexecution timeout4c5032e5dinvalid number of calls7 times (1 ms and 5 ms), ok 295c429f99aexecution timeout5 times (1 ms), ok 31, neverinvalid number of callsflatCallTracerstops hearing from the EVM as soon asStopfires — itsOnEnter/OnExitreturn early and never reach the innercallTracer, which is the only place that pops the callstack — so an interruption inside a nested call leaves more than one frame behind.4c5032e5drequired exactly one frame and reported that state asinvalid number of calls, hiding the reason;5c429f99aflattens the first frame and returns the reason, which is what the baseline and the upstream implementation do.Manual test plan: run a node on those data directories with
debugexposed and calldebug_traceBlockByNumberwithflatCallTraceron0x2418970and0x2e69c13, plusdebug_traceTransactionwithflatCallTraceron transaction 0 of0x2418971; the baseline binary fails both block traces withinvalid number of callsand reportsgasUsed0x0, this branch passes the first and reports the receipt gas. To exercise the interruption path, trace a deep transaction with{"tracer":"flatCallTracer","timeout":"1ms"}:4c5032e5dfails withinvalid number of calls,5c429f99areportsexecution timeout.Regression: on both networks both binaries imported testnet and mainnet segments normally, with no bad block, no panic and no error attributable to the change.
Types of changes
Impacted Components
callTracerandflatCallTracerreport; no state, consensus or API signature changes)Checklist
timeoutthat fires inside a nested call) keeps reporting its interruption reason, as the baseline does. No RPC method, parameter or state format changes.Relation to other work
This is the tracer half of #2579, split out so the two defects of the debug tracing surface are reviewable on their own. #2579 keeps the replay-routing refactor (
core.ApplyTransactionForReplay), thenonce too lowfix instateAtTransactionand thedebug_intermediateRootsfix; it no longer carries these two commits. The two branches touch disjoint files.#2581 removes the unconditional
IsSkipNonceTransaction()skip intraceBlockand in the state feeder of the JS tracer path, which is the reason the system-address transaction of a block is still left out ofdebug_traceBlock*(thenullentry above) and the reason a pre-fork block still fails the block trace one step later withnonce too high—invalid number of callsis fixed here, those two there.