Skip to content

GetAccount performs a redundant LockedCoins read #1202

Description

@0u-Y

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:

  • spendableSpendableCoin(addr)bankWrapper
    BankKeeper.SpendableCoin, which internally calls LockedCoins(addr);
  • lockedlockedCoin(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.

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