cosmos / cosmos/evm

GetAccount performs a redundant LockedCoins read

Open Beginner friendly
#1,202 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
164
Forks
213
Avg merge
3d 58m
Merged PRs (30d)
12

Description

## 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`.

Contributor guide

Open the contributing guide

Research direction

Start in x/vm/keeper/statedb.go by reading GetAccount, SpendableCoin, and lockedCoin, then check validateCoinInfo and setCoinInfo in x/vm/types/denom_config.go. Confirm the enforced denomination assumptions before consolidating the balance and LockedCoins reads. Done means GetAccount returns the same spendable and locked values while performing only one LockedCoins read.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, performance
Issue type
Refactor
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
75/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.