fix(traces): root-frame gas parity — Portal store byte-identical to the RPC path on traces.gas (INV-21)#153
Merged
Merged
Conversation
…he RPC path on traces.gas (INV-21) The fork's Portal-backfill trace store wrote a different `traces.gas` on the ROOT frame (traceAddress [], the top-level call of each tx) than stock ponder's RPC store: Portal serves Parity-style traces whose root `action.gas` is the tx gasLimit MINUS the EIP-2028 intrinsic, while geth's callTracer — which the RPC path stores — puts the FULL tx.gasLimit on the top frame. Grounded on a real captured Portal-vs-RPC store diff (eth mainnet): 133/133 divergences were gas-only, at the root frame, with gas_used byte-identical, and RPC.gas == the tx's full gasLimit on every root frame (no intrinsic math needed). Non-root frames' gas already matched both paths. A real fork deployment does historical backfill via Portal AND realtime tip-sync via stock ponder's untouched RPC path into the SAME database, so a path-dependent root-frame gas would leave an invisible seam at the backfill/realtime cutover. Same determinism precedent as the ancestor-error cascade (#151) and access_list #110: match the RPC path in the Portal store. buildTraces now overrides the root frame's gas with the parent tx's gasLimit (frame.gas = hx(rawTx.gas)) when traceAddress.length === 0 and the parent tx is present. Keyed on traceAddress, NOT the DFS rank — a chunk can hold only deep frames. Applied at the store-write site; the shared trace matcher (buildRawTraceMatcher/parityToCallFrame) is untouched (trace matching never reads gas). RankedTrace now carries the source traceAddress so buildTraces can identify the root. Oracle regression test from real captured data (eth block 19068568 tx 7, a Uniswap V2 swap): pre-fix Portal action.gas 0x74700 (= gasLimit − intrinsic) vs RPC/gasLimit 0x7a120; the non-root child 0x3616b already byte-identical. Mutation-verified: reverting the override fails the oracle (expected '0x74700' to be '0x7a120'). Idempotence and rootless-chunk sub-cases included. Both-version gates green (scripts/sync-upstream.sh 0.16.6 + 0.16.8 --test, 336 tests each). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the INV-21 row (root-frame gas := tx gasLimit, byte-identical to the RPC store) to the Portal invariant catalog and extend the stable-id range to INV-1…INV-21. Doc ⟷ code ⟷ test IDs kept exact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d (committee review) Blind 3-seat review (reviewer/opus SHIP; codex + glm REWORK) converged on one must-fix: the INV-21 comments/catalog stated geth seeds the top-frame gas with the full tx.gasLimit "UNCONDITIONALLY (call/create/any tx type)", but the grounding corpus (2196 root frames across a captured Portal-vs-RPC store diff) contained no contract-creation and no access-list root frames. Soften all three sites to state the identity is confirmed on legacy + EIP-1559 (intrinsic-independent) and that create/access-list rest on the geth callTracer mechanism, not a direct measurement. Also harden the root-frame guard `rawTx?.gas !== undefined` → `!= null`: skip the override rather than write a fabricated gas if a tx ever lacked a gas value (unreachable on real Portal data — gas is a required field; `hx(null)` would return '0x0' — but the looser guard states the intent precisely). No change to the override itself or any test outcome. Both-version graft test suite 336/336; biome clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Summary
The fork's Portal-backfill trace store wrote a different
traces.gason the root frame (traceAddress[], the top-level call of each tx) than stock ponder's RPC store. This is the unresolved second half of the T-eth traces divergence arc — theerror/revert_reasonhalf was fixed in #151 (INV-20). It resolves to a code fix under the same intra-deployment determinism precedent.Grounding (real captured Portal-vs-RPC store diff, eth mainnet)
A per-window diff of the two
ponder_sync.tracesstores (both@subsquid/ponder@0.16.8-sqd.1) surfaced exactly two signatures:gasonly[]);gas_usedbyte-identical;RPC.gas − Portal.gas == the tx's EIP-2028 intrinsicon every rowerror+revert_reasonThe deciding fact:
RPC.gas == the transaction's full gasLimiton every root frame (164/164, zero exceptions) — the RPC path's top-frame gas is the tx gasLimit unconditionally, with no intrinsic arithmetic. Portal's rootaction.gasisgasLimit − intrinsic. Non-root frames' gas already matched both paths (0 divergences).Mechanism
Portal serves Parity-style traces. On the root frame, Parity's
action.gas= the gas available to the call =tx.gasLimit − intrinsic(base 21000 + 16/nonzero + 4/zero calldata byte + access-list + creation costs). geth'scallTracer— which ponder's RPC path stores — puts the fulltx.gasLimiton the top frame. The fork mappedgas = a.gas ?? t.callGas(portal-transform.ts), so it carried Parity's reduced value.Determinism rationale
A real fork deployment does historical backfill via Portal and realtime tip-sync via stock ponder's untouched RPC path into the same database. A path-dependent root-frame gas would leave an invisible seam at the backfill/realtime cutover — unacceptable for a byte-identity guarantee. Same precedent as the error cascade (#151, INV-20) and access_list (#110): match the RPC path in the Portal store.
The fix
buildTraces(portal-assemble.ts) overrides the root frame's gas with the parent tx's gasLimit (frame.gas = hx(rawTx.gas)) whentraceAddress.length === 0and the parent tx is present:traceAddress, not the DFS rank — a chunk can legitimately hold only deep frames (verified: 48 of the window's tx-groups have no root frame present), so a rank-0 heuristic would corrupt a partial tree.buildRawTraceMatcher/parityToCallFrame) is untouched, and trace matching never reads gas, so the kept-set is unchanged.RankedTracenow carries the sourcetraceAddresssobuildTracescan identify the root cleanly.Blast radius
After INV-21 (root gas) + #151 (error cascade), the
tracestable is fully byte-identical across the two store paths in the grounded window — zero residual column divergences. These were the only two trace signatures the diff-harness ever surfaced. Root-frame gas is the last derived-normalization gap the Portal assembly skipped vs the RPC path.Tests
Oracle regression test built from real captured data (eth block 19068568 tx 7, a Uniswap V2 router swap):
action.gas0x74700(= gasLimit − intrinsic) vs RPC/gasLimit0x7a1200x3616b, already byte-identical (asserted untouched)Plus sub-cases: (a) an already-matching root (Parity
action.gas== gasLimit) is unchanged — idempotent; (b) a rootless chunk (only a deep frame) is NOT rewritten.Mutation-verified: reverting the override (guard never fires ⇒ Parity
action.gasat root) fails the oracle withexpected '0x74700' to be '0x7a120'; sub-cases (a)/(b) still pass (they are idempotence/scoping guards, not the sentinel).Gates
scripts/sync-upstream.sh 0.16.6 --test→ 336 passed, EXIT 0scripts/sync-upstream.sh 0.16.8 --test→ 336 passed, EXIT 0portal-assemble.test.ts: 16 → 19 testsnpx @biomejs/biome@2.5.2 check portal/ --diagnostic-level=error→ cleanINV-21 catalog row added to
portal/INVARIANTS.md(doc ⟷ code ⟷ test IDs exact).🤖 Generated with Claude Code