Summary
Keeper.GetAccount (x/vm/keeper/statedb.go) reads an account's locked
balance from the bank store twice per call — once inside SpendableCoin
and once inside lockedCoin. Under the coin configuration that x/vm
enforces, both reads return the same value, so one of them is redundant.
Context
GetAccount is on the EVM execution hot path; it is called multiple times
per transaction (state-object load, ante fee handling, and CanTransfer).
It derives two values:
- spendable —
SpendableCoin(addr) → bankWrapper →
BankKeeper.SpendableCoin, which internally calls LockedCoins(addr);
- locked —
lockedCoin(addr) → BankKeeper.LockedCoins(addr).
As a result, a single GetAccount performs one balance read and two
LockedCoins reads of the same account.
The locked snapshot itself is intentional: it was introduced in #1187 so
that spendable + locked stay correct after a precompile (e.g. staking
delegation) changes the locked amount mid-execution. The locked value
must therefore be preserved — it cannot be dropped. But the second store
read can be avoided by reading the balance and LockedCoins once and
deriving both values:
locked = lockedCoins.AmountOf(evmDenom)
spendable = balance - locked
x/vm/types/denom_config.go (validateCoinInfo) enforces
Denom == ExtendedDenom and 18 decimals only (checked in the only setter,
setCoinInfo), so the regular- and extended-denom amounts are identical,
and spendable = balance - locked matches the bank's spendable
computation, including its panic-on-negative behavior. The returned
spendable/locked values are unchanged.
Evidence
The redundancy is visible by inspecting GetAccount and holds regardless of
workload or hardware: each extra LockedCoins read is an additional account
fetch + decode, paid on every GetAccount call (several times per tx).
Profiling a transfer-heavy block confirms it as a measurable share of
GetAccount's allocations.
Impact
- Severity: low — performance only, no correctness change.
- User scope: all EVM transactions (
GetAccount runs on every tx,
multiple times).
- Downstream: fewer allocations and less GC pressure during block
execution. The win is modest and workload-dependent, but applies to
every transaction; it is also relevant to parallel execution
(Block-STM), where GC is a shared bottleneck.
Proposed fix
Read the balance and LockedCoins once in a small keeper helper and derive
both spendable and locked, in place of the separate SpendableCoin +
lockedCoin calls in GetAccount.
Summary
Keeper.GetAccount(x/vm/keeper/statedb.go) reads an account's lockedbalance from the bank store twice per call — once inside
SpendableCoinand once inside
lockedCoin. Under the coin configuration that x/vmenforces, both reads return the same value, so one of them is redundant.
Context
GetAccountis on the EVM execution hot path; it is called multiple timesper transaction (state-object load, ante fee handling, and
CanTransfer).It derives two values:
SpendableCoin(addr)→bankWrapper→BankKeeper.SpendableCoin, which internally callsLockedCoins(addr);lockedCoin(addr)→BankKeeper.LockedCoins(addr).As a result, a single
GetAccountperforms one balance read and twoLockedCoinsreads of the same account.The locked snapshot itself is intentional: it was introduced in #1187 so
that spendable + locked stay correct after a precompile (e.g. staking
delegation) changes the locked amount mid-execution. The locked value
must therefore be preserved — it cannot be dropped. But the second store
read can be avoided by reading the balance and
LockedCoinsonce andderiving both values:
x/vm/types/denom_config.go(validateCoinInfo) enforcesDenom == ExtendedDenomand 18 decimals only (checked in the only setter,setCoinInfo), so the regular- and extended-denom amounts are identical,and
spendable = balance - lockedmatches the bank's spendablecomputation, including its panic-on-negative behavior. The returned
spendable/locked values are unchanged.
Evidence
The redundancy is visible by inspecting
GetAccountand holds regardless ofworkload or hardware: each extra
LockedCoinsread is an additional accountfetch + decode, paid on every
GetAccountcall (several times per tx).Profiling a transfer-heavy block confirms it as a measurable share of
GetAccount's allocations.Impact
GetAccountruns on every tx,multiple times).
execution. The win is modest and workload-dependent, but applies to
every transaction; it is also relevant to parallel execution
(Block-STM), where GC is a shared bottleneck.
Proposed fix
Read the balance and
LockedCoinsonce in a small keeper helper and deriveboth spendable and locked, in place of the separate
SpendableCoin+lockedCoincalls inGetAccount.