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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions