Skip to content

fix(vm,erc20): decode pre-migration EVM state for historical queries#14

Merged
AkKoks merged 4 commits into
tac/v0.6.0from
fix/historical-eth-call-legacy-decode
Jul 2, 2026
Merged

fix(vm,erc20): decode pre-migration EVM state for historical queries#14
AkKoks merged 4 commits into
tac/v0.6.0from
fix/historical-eth-call-legacy-decode

Conversation

@AkKoks

@AkKoks AkKoks commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Historical eth_call (and estimateGas/traces) below the store-migration height failed or returned wrong results, because pre-migration state is decoded with the current, incompatible layout:

  • x/vm Params: proto field numbers shifted (old field 10 active_static_precompiles vs new history_serve_window), so decoding panics with "wrong wireType = 2 for field HistoryServeWindow".
  • x/erc20 precompiles: the native/dynamic lists moved from a single concatenated-blob key to per-address keys, so precompiles appear unregistered at historical heights and calls execute the decoy ERC20 bytecode (returning 0 or reverting) instead of the precompile.

Add a lazy, height-gated shim on the read path:

  • utils.LazyUpgradeHeight resolves and caches the migration height from a caller-supplied resolver (nil = disabled, keeping the modules generic).
  • x/vm GetParams decodes the legacy Params layout below that height via types.DecodeLegacyParams.
  • x/erc20 IsNative/IsDynamicPrecompileAvailable fall back to the legacy concatenated-blob keys below that height.

Query-path only: on current heights the code path is byte-for-byte unchanged, so this is not consensus-affecting and ships as a plain binary update. The resolver is wired from the applied x/upgrade done-height on the app side.

Description

Closes: #XXXX


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • tackled an existing issue or discussed with a team member
  • left instructions on how to review the changes
  • targeted the main branch

Historical eth_call (and estimateGas/traces) below the store-migration
height failed or returned wrong results, because pre-migration state is
decoded with the current, incompatible layout:

- x/vm Params: proto field numbers shifted (old field 10
  active_static_precompiles vs new history_serve_window), so decoding
  panics with "wrong wireType = 2 for field HistoryServeWindow".
- x/erc20 precompiles: the native/dynamic lists moved from a single
  concatenated-blob key to per-address keys, so precompiles appear
  unregistered at historical heights and calls execute the decoy ERC20
  bytecode (returning 0 or reverting) instead of the precompile.

Add a lazy, height-gated shim on the read path:

- utils.LazyUpgradeHeight resolves and caches the migration height from a
  caller-supplied resolver (nil = disabled, keeping the modules generic).
- x/vm GetParams decodes the legacy Params layout below that height via
  types.DecodeLegacyParams.
- x/erc20 IsNative/IsDynamicPrecompileAvailable fall back to the legacy
  concatenated-blob keys below that height.

Query-path only: on current heights the code path is byte-for-byte
unchanged, so this is not consensus-affecting and ships as a plain
binary update. The resolver is wired from the applied x/upgrade
done-height on the app side.
@github-actions github-actions Bot added the tests label Jul 2, 2026
AkKoks added 3 commits July 2, 2026 20:35
The migration height was resolved lazily from the query context. For a
historical eth_call below the migration height, that context predates the
upgrade, so GetDoneHeight returned 0 and the legacy-decode paths were
skipped (params happened to work only because GetParams warms its cache
every block via BeginBlock; erc20 precompile-availability is not hit every
block, so its cache stayed cold and gTAC.balanceOf at old heights returned
0 instead of the real balance).

Add PrimeLegacyParamsHeight / PrimeLegacyPrecompilesHeight to warm both
gates from a current-height context; the app calls them in BeginBlocker.
Once the upgrade is applied the height is cached and reused by historical
reads. Verified end-to-end (v1.0.4 -> v1.6.0) on real pre-upgrade state.
Move the per-block cache warming into the x/vm module's BeginBlock instead
of relying on the host app's BeginBlocker. BeginBlock now primes its own
legacy Params height and, via the Erc20Keeper interface, the erc20
precompile-availability height. Keeps the whole historical-decode fix
inside the fork; the host app no longer needs a warming hook.
erc20Keeper is optional in the vm keeper (GetPrecompileInstance guards
nil), so keep BeginBlock consistent and nil-safe for chains that wire the
vm keeper without x/erc20.
@AkKoks AkKoks force-pushed the fix/historical-eth-call-legacy-decode branch from 327931e to 5796b41 Compare July 2, 2026 14:45
@AkKoks AkKoks merged commit 98d59f6 into tac/v0.6.0 Jul 2, 2026
18 of 38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant