Skip to content

Transient storage as its own address space - #8

Open
halaprix wants to merge 4 commits into
masterfrom
feat/transient-storage
Open

Transient storage as its own address space#8
halaprix wants to merge 4 commits into
masterfrom
feat/transient-storage

Conversation

@halaprix

Copy link
Copy Markdown
Owner

Implements docs/design/transient-storage-v1.5.md. Closes slotscope-syp (.1.3).

Why it is a separate space, not a second column

Transient storage (EIP-1153) numbers from 0 in its own address space. In the
probe that shaped this design, the persistent persistent is slot 0 and the
transient lock is slot 0. A merged view would make slot 0 mean two things at
once, so transient gets its own tab and its own grid. Keeping them apart is the
lesson, not an implementation detail — there is an e2e that asserts neither grid
ever shows the other's name.

What solc actually gives us

Probed against the locked solc 0.8.36 before designing anything, because the
whole shape depended on it:

  1. transientStorageLayout already exists, same shape as storageLayout. So
    decodeStorageLayout and buildRows work on it verbatim — no new decoder. It
    simply was not in our outputSelection.
  2. Packing is identical: two uint128 transient variables shared slot 2 at offsets
    0 and 16.
  3. Value types only. mapping, structs and static arrays are rejected with
    UnimplementedFeatureError: Transient data location is only supported for value types.

Point 3 is why this phase is small: the transient grid needs none of the v1.2/v1.4
reference machinery — no reserved heads, no keccak-derived children, no elided
ranges. StorageGrid is reused unchanged behind a space prop that selects copy
and switches derived-child reconstruction off, since reconstructing a transient
slot against persistent keccaks could only invent a link.

Transient accesses are finally named

docs/limitations-v1.0.md recorded that transient accesses never resolve. That was
true only while the persistent layout was the sole index — naming them against it
would have named the wrong variable. resolveAccesses now takes an optional
transient layout and routes strictly by kind, so neither index is ever consulted
for the other space's accesses. Without this the tab could label nothing.

Honesty about lifetime

Transient state is discarded when the transaction ends, so the tab never claims a
"current" value. Two permanent text lines carry it, not styling:

  • before a run — transient storage is empty outside a transaction;
  • after one — observed during the transaction · discarded when it ended.

The tab itself appears only when the contract declares transient variables, so its
absence is compiler truth rather than a UI accident.

Note on the locked input

The Standard JSON snapshot moved deliberately to request the new output. Fixture
goldens verify unchanged, which confirms the added selection changes what solc
reports, not what it compiles — the property that matters for a locked
environment.

Verification

Gate Result
format:check, lint, typecheck clean
test:unit 247/247, including real compiled contracts through the EVM for the transient resolver
test:e2e 65/65 — 3 new transient specs, including axe and a both-spaces-have-slot-0 case
fixtures:verify no drift
release:verify passed
build:check lean main entry
benchmark:v0.1 p95 218 ms action-to-paint, 689 ms cold start — PASS

🤖 Generated with Claude Code

halaprix and others added 4 commits July 30, 2026 16:58
slotscope-zvd gave every reserved slot a row but could not show what lives in a
mapping, because those entries sit near 2^256, nowhere near the declared
sequence. They now appear as child rows under the head they descend from.

The address is written the way the EVM computes it — keccak(0xaa…, 1), or
keccak(0xbb…, keccak(0xaa…, 2)) for a nested hop, or keccak(3)+2 for an array
element — and that string goes in the slot rail, which is the address column
everywhere else in this instrument. The raw 32-byte slot is a hover detail. The
derivation is the identity, because it is the thing worth learning.

Every child is reconstructed from Layer-1 facts alone: KeccakObservation carries
the exact preimage, so a 64-byte input splits into key and base, a 32-byte input
is an array or data head, and a nested hop is followed until its tail is a
declared slot. Parent linkage is therefore slot arithmetic, not name matching —
a head nested inside a struct attaches with no special case, and the
derivedCountByBase name-matching gap from v1.2 disappears rather than being
patched.

buildRows stays compiler-only: the gapless invariant is about DECLARED slots, and
a derived slot is not one. Children attach at the view layer.

Decisions worth keeping:
- The child set spans the whole run rather than stopping at the cursor. Roving
  focus is index-based, so a row appearing mid-scrub would move focus under the
  reader; the cursor changes values, never row positions.
- Read-only entries are included and marked. Nothing surfaced them before, and
  "read but never written" is worth teaching.
- A touched slot whose derivation cannot be reconstructed becomes a visible
  orphan, never a silent drop.
- RuntimeStorage becomes a write log in observation order. The grid is the map;
  two maps of the same state only made a reader pick one to believe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Transient storage (EIP-1153) gets its own tab and its own grid. It is never
merged with persistent storage, because both spaces number from 0 — in the probe
that shaped this design, the persistent `persistent` is slot 0 and the transient
`lock` is slot 0 — so a merged view would make slot 0 mean two things at once.
Keeping them apart is the lesson, not an implementation detail.

Probed against the locked solc 0.8.36 before designing anything:

- transientStorageLayout exists with the same shape as storageLayout, so
  decodeStorageLayout and buildRows work on it verbatim;
- packing is identical (two uint128 transient vars share slot 2 at 0 and 16);
- transient is VALUE TYPES ONLY — mapping, struct and static array are rejected
  with UnimplementedFeatureError.

That last point is why this phase is small: the transient grid needs none of the
v1.2/v1.4 reference machinery. No reserved heads, no derived children, no elided
ranges. StorageGrid is reused unchanged behind a `space` prop that only selects
copy and switches derived-child reconstruction off, since reconstructing against
persistent keccaks could only invent a link.

Transient accesses are now named: resolveAccesses takes an optional transient
layout and routes strictly by kind, so neither index is ever consulted for the
other space's accesses. docs/limitations-v1.0.md is updated accordingly — that
entry was true only while the persistent layout was the sole index.

Honesty about lifetime is carried in permanent text, not styling. Before a run:
"transient storage is empty outside a transaction". After one: "observed during
the transaction, discarded when it ended". The tab itself appears only when the
contract declares transient variables, so its absence is compiler truth rather
than a UI accident.

The locked Standard JSON snapshot moved deliberately to request the new output.
Fixture goldens verify unchanged, which confirms the added selection changes what
solc reports, not what it compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
slotscope Ready Ready Preview Jul 30, 2026 8:33pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant