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):
- L158 —
stakingKeeper.IterateRedelegations(...): iterates all redelegations on the chain, filters ValidatorSrcAddress/DstAddress == oldValAddr inside the Go callback.
- L244 —
distributionKeeper.IterateValidatorHistoricalRewards(...): iterates all validators' historical rewards, filters val.Equals(oldValAddr) in the callback.
- L263 —
distributionKeeper.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
Summary
MsgMigrateValidatorperforms 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-2with 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:
ValuePerByteBDB4EB678AB4A68786F067F9FCAB186C06BC52A30A635F942A1697055843A31FWriteFlatF3F0E6692D8DF9D2488A89D76F2B617D0BAEAE4B7EFB28781A84550BE21CDFA6IterNextFlatD82E1FE3722B61F068CC42745B2D48A12CECE6CA15C78EFD8FCEF50FB446DD71ValuePerByte7B2E8838E050285592EBC64AD920FAB2432FD885FB2C1C43E354CBE7B00349337110C40CE70B871F44CE63704D4F2604B61BFE602F51CAE1E90A74AD5408F85ASide effects observed:
Root cause
x/evmigration/keeper/migrate_validator.go(v1.20.0):stakingKeeper.IterateRedelegations(...): iterates all redelegations on the chain, filtersValidatorSrcAddress/DstAddress == oldValAddrinside the Go callback.distributionKeeper.IterateValidatorHistoricalRewards(...): iterates all validators' historical rewards, filtersval.Equals(oldValAddr)in the callback.distributionKeeper.IterateValidatorSlashEvents(...): same pattern.The callbacks always
return false, so the iterator walks the entire store; every record read chargesReadCostPerByteagainst 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
Suggested fix
oldValAddr— the distribution store is already keyed by validator prefix, soIterateValidatorHistoricalRewards/SlashEventscan be ranged scans; redelegations can use the per-validator (src/dst) index instead of the global iterator.Happy to test a patched binary on testnet.
Validator:
coinsspor/ legacylumera1jfl8rf4pmf9pwzfzckl6pv8jmz0fqp2t3lk0xv/ newlumera1gxauzl4ssrdtqznra9dcwn4cz9lq0qsqrdl9ka