Skip to content

[Bug] MsgMigrateValidator: unbounded chain-wide store iteration under tx gas meter — large validators cannot migrate #182

Description

@coinsspor

Summary

MsgMigrateValidator performs three chain-wide store iterations under the tx gas meter, so migration gas cost scales with total chain state, not with the validator's own record count. Large validators burn 100% of any provided gas limit and cannot complete migration.

Tested on lumera-testnet-2 with a validator holding 5087 delegations (0 unbonding, 0 redelegations), after the cap raise to 7500 (prop 13).

Reproduction / evidence

Every attempt consumed the entire gas limit and failed just above it, at iteration/read cost locations:

Gas provided Result Location Tx hash
20M out of gas (gasUsed 20,000,524) ValuePerByte BDB4EB678AB4A68786F067F9FCAB186C06BC52A30A635F942A1697055843A31F
50M out of gas (gasUsed 50,001,404) WriteFlat F3F0E6692D8DF9D2488A89D76F2B617D0BAEAE4B7EFB28781A84550BE21CDFA6
500M out of gas (gasUsed 500,000,005) IterNextFlat D82E1FE3722B61F068CC42745B2D48A12CECE6CA15C78EFD8FCEF50FB446DD71
2B out of gas (gasUsed 2,000,000,087) ValuePerByte 7B2E8838E050285592EBC64AD920FAB2432FD885FB2C1C43E354CBE7B0034933
20B broadcast accepted, never landed in a block — appears silently dropped from mempools 7110C40CE70B871F44CE63704D4F2604B61BFE602F51CAE1E90A74AD5408F85A

Side effects observed:

  • Broadcasting via the local node stalls it during CheckTx (validator missed blocks, required restart).
  • CheckTx times out public RPC gateways (504 from two independent providers).
  • The 2B execution visibly slowed block production network-wide.

Root cause

x/evmigration/keeper/migrate_validator.go (v1.20.0):

  • L158stakingKeeper.IterateRedelegations(...): iterates all redelegations on the chain, filters ValidatorSrcAddress/DstAddress == oldValAddr inside the Go callback.
  • L244distributionKeeper.IterateValidatorHistoricalRewards(...): iterates all validators' historical rewards, filters val.Equals(oldValAddr) in the callback.
  • L263distributionKeeper.IterateValidatorSlashEvents(...): same pattern.

The callbacks always return false, so the iterator walks the entire store; every record read charges ReadCostPerByte against the tx gas meter. On a long-running chain the historical-rewards store alone is millions of records, which matches the observed failure locations (IterNextFlat, ValuePerByte).

Impact

  • Any validator above a modest size cannot migrate, regardless of gas.
  • Cost grows with chain state over time, while the migration window is fixed (~3 months) — worst case, large validators permanently lose delegations per the announced deadline semantics.
  • Mainnet state is larger, so this hits mainnet harder than testnet.
  • Heavy CheckTx/execution of a single fee-free tx measurably slows the chain (mild DoS surface).

Suggested fix

  • Prefix-scope the iterations to oldValAddr — the distribution store is already keyed by validator prefix, so IterateValidatorHistoricalRewards/SlashEvents can be ranged scans; redelegations can use the per-validator (src/dst) index instead of the global iterator.
  • Alternatively, run migration under an infinite gas meter (migration txs are already fee-free/signature-free) — though prefix scoping is the cleaner fix.

Happy to test a patched binary on testnet.
Validator: coinsspor / legacy lumera1jfl8rf4pmf9pwzfzckl6pv8jmz0fqp2t3lk0xv / new lumera1gxauzl4ssrdtqznra9dcwn4cz9lq0qsqrdl9ka

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions