Expand description
Targeted state-mutation vocabulary and the structured diff it produces (Pillar B.1 — the writer half of the event → state pipeline).
This module defines the small, generic vocabulary a future event decoder
emits and EvmCache::apply_update
consumes, plus the StateDiff that records what an apply actually changed.
It is pure data and logic on itself: it carries no protocol or event
knowledge and has no dependency on the cache implementation.
§The vocabulary
A StateUpdate is one targeted mutation:
StateUpdate::Slot— set a single storage slot, authoritative across both cache layers.StateUpdate::Account— apply a partialAccountPatch(balance/nonce/code, each optional) to an already-known account.StateUpdate::AccountUpsert— intentionally materialize a cold account from a partialAccountPatch.StateUpdate::Purge— drop cached state at aPurgeScopeso the next read re-fetches.
§The dual-layer write-through policy
apply_update applies Slot writes
through with one consistent rule: the BlockchainDb backend (layer 2) is
written always; the CacheDB overlay (layer 1) is written only if an
overlay account already exists for the address. A new overlay account is
never materialized for a slot write — the read path falls through to the
backend for an absent overlay entry, so a backend-only write is authoritative,
and materializing an overlay entry would pollute layer 1 and could shadow
later RPC reads. (This mirrors the established
inject_storage_batch_fresh
semantics.)
Account patches follow the same overlay-if-present write-through policy
once the account is already present in either layer. If the account is absent
from both layers, the patch is skipped and surfaced in
StateDiff::skipped_accounts. Use StateUpdate::AccountUpsert when the
caller intentionally wants to materialize a cold/default account.
§The output
Every apply returns a StateDiff of the changes it actually made: the
SlotChanges, AccountChanges, and PurgeRecords. Only real changes
are recorded — re-applying a value the cache already holds yields an empty
diff, so idempotence is observable.
§Relative updates / cold-aware read-modify-write
Some callers learn only a delta (an ERC-20 Transfer log carries the
transferred amount, not the resulting balances), so the vocabulary also
supports relative updates: StateUpdate::SlotDelta reads the current slot
value, applies a saturating SlotDelta (Add clamps at U256::MAX, Sub
at U256::ZERO), and writes the result back through both layers. The general
closure form is
EvmCache::modify_slot.
A relative update is only valid against a value the cache actually holds. An
un-fetched (“cold”) slot has no value, and applying a delta to it would compute
0 ± amount, write a wrong value, and (write-through) make it authoritative —
silently corrupting state. So relative application is cold-aware: a
SlotDelta on a cold slot is not applied; it is recorded in
StateDiff::skipped as a SkippedDelta so the caller can fetch+seed the
true value (the next read otherwise lazily fetches it). modify_slot hands its
closure an Option<U256> (None when cold) and lets the caller decide.
§Masked writes to packed words
A storage slot often packs several fields into one word. A decoder that
learns only some of those fields must update just its bits without
clobbering the rest. StateUpdate::SlotMasked is the
cold-aware masked read-modify-write for exactly this: it computes
new = (old & !mask) | (value & mask), touching only the mask bits. Like
SlotDelta it is cold-aware — the un-masked bits of
a slot absent from both layers are unknown, so a masked write to a cold slot
is not applied; it is surfaced in StateDiff::skipped_masks as a
SkippedMask. (A full-mask SlotMasked matches an absolute
Slot write on a hot slot but still skips on a cold one.)
The same relative, cold-aware rule extends to an account’s native balance:
StateUpdate::BalanceDelta (and the closure form
EvmCache::modify_account_balance)
read-modify-write AccountInfo::balance, preserving nonce/code. “Cold” here
means the account is absent from both layers (its balance is unknown); a
BalanceDelta on a cold account is not applied — it is surfaced in
StateDiff::skipped_balances as a SkippedBalanceDelta. This avoids
materializing a default account that would mask the real on-chain one.
§Checking for skips
Because a cold-skipped relative update produces no change, it is invisible
to the natural StateDiff::is_empty / StateDiff::len success check (those
are changes-only). A caller applying relative updates must therefore check
StateDiff::has_skipped (or inspect skipped,
skipped_balances,
skipped_masks, or
skipped_accounts) — a cold target was
dropped, not applied, and a silently-dropped balance/account update can break
conservation. StateDiff::is_fully_applied and
StateDiff::skipped_len are the companions.
§Boundary — events are Phase 4
This is the vocabulary a Phase 4 EventDecoder will emit into; Phase 3
does not decode events. Nothing here parses a Log or knows a protocol’s
storage layout — that is the reader half of Pillar B and lands later.
Structs§
- Account
Change - An account field delta. Each field is
Some((old, new))only when it changed. - Account
Patch - A partial account mutation: each
Somefield overwrites the cached value, eachNoneleaves it unchanged. Settingcoderecomputes the code hash;Some(empty bytes)clears code to the empty-code hash. - Purge
Record - Record of a purge: how much of each layer it removed.
- Skipped
Account Patch - An account patch (
StateUpdate::Account) that could not be applied because the account’s info is unknown to the EVM — absent from both cache layers, or present in the overlay as revmNotExisting. - Skipped
Balance Delta - A relative balance update (
StateUpdate::BalanceDelta) that could not be applied because the account’s info is unknown to the EVM — absent from both cache layers, or present in the overlay as revmNotExisting(so its native balance is unknown). - Skipped
Delta - A relative update (
StateUpdate::SlotDelta) that could not be applied because the slot’s current value is unknown (not cached in either layer). - Skipped
Mask - A masked write (
StateUpdate::SlotMasked) that could not be applied because the target slot’s current value is unknown (not cached in either layer). - State
Diff - What an
apply_*call actually changed.
Enums§
- Purge
Scope - What part of an address’s cached state a purge removes.
- Slot
Delta - A relative storage-slot mutation: read the current value, transform it, and write it back.
- State
Update - A single targeted mutation to cached EVM state.