fix(traces): ancestor-error cascade — Portal store byte-identical to the RPC path on traces.error/revert_reason#151
Merged
Conversation
…istorical assembly Stock ponder's debug_traceBlockByNumber/ByHash DFS (src/rpc/actions.ts, dist/esm/rpc/actions.js:100-118) smears a reverted ancestor's error/revertReason onto every descendant lacking its OWN error, transitively (an inheriting frame becomes a source for its own children); a frame with its own error keeps it. The Portal historical path builds trace frames itself (parityToCallFrame) from Portal's geth-faithful per-frame errors and never invokes that DFS, so a succeeded child of a reverted parent was stored with error=null where the RPC path inherits the parent's error — a traces.error/revert_reason divergence between the two store paths. Realtime/fallback already flow through the untouched RPC cascade, so the fork DB must be uniformly ponder-shaped. rankTraces now applies the same cascade over the DFS-sorted (cmpTraceAddr) frames of each tx, keyed by traceAddress prefix (parent of [a,b,c] is [a,b]). Applied only here; the realtime path is untouched and cannot double-apply. Oracle regression test over a real reverted tx (eth block 20351061, tx 26) plus synthetic own-error-survives / 3-deep-transitive / non-reverted-stays-null cases; mutation-verified to fail on the pre-shim code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… — INVARIANTS row, INV-20(d), byAddr robustify, candor doc Scoped follow-up polish on the ancestor-error trace cascade (core algorithm unchanged; already committee-proven byte-identical to stock ponder's rpc/actions.ts DFS). - Reword the shipped placeholder: the JSDoc's `<UPSTREAM_ISSUE_URL> (to be filled by the reviewer)` now points to "Upstream divergence documented in VALIDATION.md §5" (no invented URL). - Renumber the cascade invariant INV-19 → INV-20 across code/test/doc. INV-19 was ALREADY taken by the code-enforced low-edge chunk-coverage guard from #50 (portal.ts invariant("INV-19", …), tested in portal.test.ts). Squatting on INV-19 broke the doc⟷code⟷test grep-ability rule (two unrelated invariants under one id); INV-20 is the next free id. - Add the INV-20 catalog row to portal/INVARIANTS.md (matches the table's column format; cites cascadeTraceErrors/rankTraces + the INV-20 tests + VALIDATION.md §5). - Robustify the byAddr test helper: assert ranked.length === sorted.length so the positional zip stays a valid oracle instead of silently misaligning if a future fixture ever introduces a parityToCallFrame-droppable trace type (safe today: all type:'call'). - Add a non-prefix-closed candor doc-comment above cascadeTraceErrors: an orphan (missing intermediate ancestor) inherits nothing — a deliberate conservative choice (never fabricate a false smear; no worse than the pre-shim null), unreachable on geth-faithful prefix-closed Portal trees. - Add test INV-20 (d): an inheriting child of an ancestor with an error but NO revertReason gets error set AND revertReason === undefined — pinning the UNCONDITIONAL revertReason assignment that keeps the Portal store byte-exact with upstream rpc/actions.ts. No guard added on frame.revertReason (a guard would diverge the Portal store from the RPC store). Gates: sync-upstream.sh --test 0.16.6 + 0.16.8 both 333 passed / 18 files, build OK; biome 2.5.2 clean at error level. Mutation-verified: neutering the applied cascade fails INV-20 / (a) / (b) / (d) ("expected undefined to be 'execution reverted'") while (c) stays green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Stock ponder derives trace
error/revertReasonin itsdebug_traceBlockByNumber/debug_traceBlockByHashDFS (src/rpc/actions.ts, shipped asdist/esm/rpc/actions.js:100-118): within one transaction's trace tree, a reverted ancestor'serror/revertReasonis smeared onto every descendant that does not have its own error, transitively — a frame that inherits becomes a source for its own children. A frame with its own distinct error keeps it (own-error branch wins).The Portal historical path builds its trace frames itself via
parityToCallFramefrom Portal's per-frame errors and never invokes that DFS, so a succeeded child of a reverted parent was stored witherror = null, whereas the RPC path stores the inherited parent error. That is atraces.error/traces.revert_reasondivergence between the two store paths of this same package — realtime and the realtime→getLogs fallback already flow through the untouched stock RPC cascade, so the store must be uniformly ponder-shaped regardless of which path wrote a row.Candor framing: Portal's raw trace data was verified byte-identical to geth callTracer ground truth; the RPC-path store's differing values come from stock ponder's derived ancestor-error post-processing (
src/rpc/actions.ts), which this change replicates for drop-in parity. Both readings are defensible — the RPC/store value reflects effects-reverted semantics (a call inside a reverted tx has no committed effect), the raw geth value reflects frame-local execution. This PR does not take a side on which is "correct"; it makes the Portal store match the stock store so the package is drop-in deterministic across paths.The mechanism replicated (code-cited)
dist/esm/rpc/actions.js(andsrc/rpc/actions.ts), per frame in DFS order:error !== undefined→ frame is a source, keep ownerror/revertReason;error/revertReasonand become a source (transitive);Where it lands
portal/portal-assemble.tsrankTracesalready sorts each tx's traces into DFS pre-order (cmpTraceAddr, parent-before-child). The cascade runs over that sorted list, keyed by traceAddress prefix (parent of[a,b,c]is[a,b]) instead of upstream's frame-identityparentFramethreading — the frames within a tx are exactly one tree and DFS order resolves each parent before its children. Applied only in the Portal historical assembly; the realtime path (which uses the stock RPC actions) is untouched and cannot double-apply.Blast-radius check (STEP 0)
The upstream DFS derives exactly three things per frame: (1) the
error/revertReasoncascade, (2)frame.index(per-tx DFS pre-order, reset to 0 per top-level trace), and (3)frame.subcalls. The fork already reproduces (2) viarankTraces(cmpTraceAddrsort = DFS pre-order, per-tx, 0-based) and (3) viaparityToCallFrame'ssubcalls: subtraces ?? 0(Paritysubtraces= immediate-child count = gethcalls.length).standardizeTraceonly validates and defaultsgas/gasUsed. The error/revertReason cascade is therefore the only derived normalization the Portal assembly was skipping — this PR closes exactly that gap and nothing else.Tests (oracle-based) + mutation verification
portal/portal-assemble.test.ts, new INV-19 group, over a co-located fixture derived from a real reverted transaction (eth mainnet block 20351061, tx index 26 — a reverted swap-router call), reshaped into Portal's per-frame trace shape with each frame's own error preserved verbatim:[0,1]frame (V2 router0x7a250d…, input0x38ed1739) has no own error but its ancestor reverted, so after the cascade it carrieserror="execution reverted"/revertReason="Too little received"— and, the root having reverted, every frame in the tx ends up smeared (matching the RPC store);Mutation-verified: with the cascade disabled in the built tree, the oracle + (a) + (b) tests fail (e.g. the oracle asserts
swap.errorand getsAssertionError: expected undefined to be 'execution reverted'); (c) passes both ways as expected for an all-null control.Gates
scripts/sync-upstream.sh 0.16.6 --test→ 332 passed (18 files), build OK.scripts/sync-upstream.sh 0.16.8 --test→ 332 passed (18 files), build OK.@biomejs/biome@2.5.2 check portal/→ clean at ERROR level.The shim carries a
<UPSTREAM_ISSUE_URL>placeholder to be filled with the upstream issue link.