Skip to content

rpc: remember deployed accounts instead of re-reading getClassHashAt - #741

Merged
derek-flossman merged 4 commits into
release/v0.10.8from
perf/cache-class-hash-at
Oct 26, 2026
Merged

derek-flossman merged 4 commits into
release/v0.10.8from
perf/cache-class-hash-at

Conversation

@adrienlacombe

@adrienlacombe adrienlacombe commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Closes #742

Targets release/v0.10.8.

Summary

Every deployment-status read the background makes is one starknet_getClassHashAt, and it repeats that read for the same address on every Home refresh (the read-only flag), fee review, approval, deploy screen, STRK20 staging step and recovery rescan. This PR remembers the answer when it is "deployed".

  • rememberDeployedAccounts wraps getDeploymentStatus in the production RuntimeRpc wiring. Once a node reports an address deployed, later reads for that address and endpoint list are answered from worker memory for 5 minutes. That's longer than the side panel's 60-second refresh.
  • Only "deployed" is remembered. A Starknet contract cannot be removed from its address. "Not deployed" and failures always reach the chain: an account can deploy with its next transaction, and a Retry must still read the chain again (Sherlock 218).
  • "Deployed" can still stop being true, so it is bounded. A devnet reset under the same URL, or a reorg of the latest block, can make a deployed address absent again (Codex review).
    • An endpoint list that includes a loopback node (127.0.0.1 or localhost, which covers the built-in SN_DEVNET) is never remembered.
    • Every other "deployed" answer is read again once it is 5 minutes old, so a reset remote devnet or a reorg is noticed within that window.
    • isLoopbackServiceUrl is exported from @bramble/core/service-url and reuses its existing loopback host set, rather than adding another copy.
  • Only existence is remembered, not the class hash. An upgrade replaces the class an account runs. The signing-path checks (isCompatibleAccountDeployed, requireLiveAuthorizableClass, the recovery signing-restriction probes) call the provider directly at a pinned block and do not go through this cache.
  • The key is the canonical address plus the exact endpoint list. The same counterfactual address on mainnet and Sepolia stays separate. A changed RPC endpoint reads again, so one endpoint's answer is not reused after the user switches away from it. Only addresses getDeploymentStatus itself accepts get a key, so a spelling it refuses (decimal, uppercase, zero) still reaches it and is refused. The map holds at most 512 addresses, drops the oldest first, and is never persisted.

Expected effect: smaller than the "~660 calls per scan" estimate

The recovery scan checks 7 candidates per index. For an undeployed candidate it makes 1 getClassHashAt plus one balance read per probe token: 2 tokens on Sepolia, 3 on mainnet. That is where "about a third of every scan" comes from.

Nearly every candidate is undeployed, and those answers must still be read live. So:

  • First scan: unchanged. Each address is read once anyway.
  • Rescan within 5 minutes: saves one read per deployed candidate.
  • Getting a scan from ~990 to ~660 calls: would require remembering "not deployed" answers. This PR deliberately doesn't. Doing so would undo the Sherlock 218 fix (Retry re-reads the chain) and could hide an account deployed between scans.

The main saving is on repeated reads of the user's own deployed accounts during normal use.

Test plan

  • packages/platform/starknet/tests/transactions/deployment-status-cache.test.ts:
    • a deployed address is read once within the window, and again after it, which is when a reset or reorg shows up as "not deployed"
    • an undeployed address, a failed read, a different endpoint list, a loopback endpoint and an address the reader refuses each read every time
    • when two reads of one address overlap, only the one started last decides what is remembered, whichever order they finish in (review feedback)
    • zero-padded spellings share one entry
    • the oldest entry is evicted at capacity
    • over the real getDeploymentStatus, a deployed account costs one getClassHashAt
  • isLoopbackServiceUrl cases in packages/foundation/core/tests/service-url.test.ts
  • bun run check

🤖 Generated with Claude Code

Every deployment-status read is one starknet_getClassHashAt, and the
background repeats it for the same address on each Home refresh, fee
review, approval, deploy screen and recovery rescan. A Starknet contract
cannot be removed from its address, so once a node reports "deployed" the
production RuntimeRpc now answers that address from worker memory.

Only "deployed" is kept: "not deployed" and failures always reach the
chain, so a fresh deployment is seen at once and a Retry still re-reads
(Sherlock 218). Only existence is kept, never the class hash: upgrades
change it, and the signing-path compatibility checks keep reading it live
at a pinned block outside this cache. Entries are keyed by canonical
address plus the exact endpoint list, bounded at 512, and never persisted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T01:57:53.828518Z f485677 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fd1194158

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/platform/starknet/src/transactions/deployment-status-cache.ts Outdated
…ocal node (#741 review)

"Deployed" can stop being true: a devnet reset under the same URL, or a
reorg of the latest block, leaves a remembered address absent while the
cache keeps answering true, and the deploy screen then refuses with
"account already deployed" until the worker restarts.

An endpoint list that includes a loopback node (127.0.0.1 or localhost,
which covers the built-in SN_DEVNET) is now never remembered, and every
other "deployed" answer is read again once it is five minutes old, longer
than the side panel's 60-second refresh. A read that then answers "not
deployed" drops the entry. isLoopbackServiceUrl is exported from the
service-url policy so its loopback host set stays single-sourced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@derek-flossman

Copy link
Copy Markdown
Collaborator

AI-assisted review (Claude, with an independent Codex gpt-6-astra pass), posted from @derek-flossman's account. Findings were checked against the code; treat them as review input, not approval.

Low — out-of-order reads. Two concurrent misses for the same key: if the earlier read ("deployed") resolves after a newer one ("not deployed"), remember(key) stores the older answer. Since deployment is one-way, this only matters after a reorg on a remote node, and the 5-minute window bounds it. Optional fix: a per-key generation counter so a superseded read can't write. Otherwise looks good.

…741 review)

Two reads of one address can overlap. If the older one answers
"deployed" and settles after a newer "not deployed" (a reorg one remote
node has already seen), it re-stored the stale answer for another five
minutes. Each read now takes a ticket for its key, and only the newest
ticket may store or clear the remembered answer; superseded reads still
return their own result to their caller. The ticket map only holds reads
in flight, since each entry is removed when its read settles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@adrienlacombe adrienlacombe left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a careful, small change. I looked mainly at how stale answers could hurt, and I didn't find a correctness problem.

What I checked

  • Network switch / endpoint change: the key is the canonical address plus the exact rpcUrls list. Switching network or editing an endpoint reads the chain again. ✅
  • Undeployed accounts: "not deployed" and failed reads are never stored, so a deploy shows up on the next read and Retry still reaches the chain (Sherlock 218). ✅
  • Class upgrade: only existence is cached. isCompatibleAccountDeployed and requireLiveAuthorizableClass still read live.
  • Home read-only flag: in account-signability.ts, a cached "deployed" still goes through the live getBraavosSigningRestriction probe. An undeployed Braavos is never cached, so the "Not deployed" flag clears on the first refresh after deployment. ✅
  • Overlapping reads: the ticket logic means only the newest read may store or clear an answer, and the tests pin both orders. ✅
  • Persistence: the cache is in worker memory only, nothing goes to storage, and it is capped at 512 entries. When the MV3 worker sleeps, the saving resets. That only costs efficiency, not correctness.

Small, non-blocking notes

  • Only 127.0.0.1 and localhost count as local. A devnet reset behind a non-loopback HTTPS host would keep "deployed" for up to 5 minutes. That's acceptable, and the module comment already says so.
  • Heads-up for merge order: #755 also edits createDefaultRuntimeRpc on the line right after getDeploymentStatus,. Whichever PR lands second gets a small textual conflict in runtime-rpc.ts.

CI: fast-checks and pr-guards pass on f485677. The branch is behind release/v0.10.8, so it needs a rebase or update before merge.

@derek-flossman

Copy link
Copy Markdown
Collaborator

Review follow-up (AI-assisted, by Claude), posted from @derek-flossman's account.

Independent check before merge. No findings.

Earlier findings are resolved in the code:

  • Codex P2 (stale "deployed" after a devnet reset or reorg):
    • an endpoint list that includes a loopback node is never remembered;
    • any other answer is re-read after 5 minutes;
    • a fresh "not deployed" clears the entry.
  • My Low from last night (out-of-order reads): fixed by the per-key ticket. Only the newest read started for an address can store or clear its answer, and the tests pin both orders.

Confirmed in code:

  • Only "deployed" is reused. "Not deployed" and failures always reach the chain (Sherlock 218).
  • The key is the canonical address plus the exact rpcUrls list.
  • The map lives in memory only, capped at 512.
  • Signing-path compatibility checks read live and don't use the cache.
  • Every background getDeploymentStatus caller goes through the one wrapped RuntimeRpc entry.
  • Worst case is a stale "deployed" for up to 5 minutes after a remote reorg. That fails safe, because an invoke from an undeployed account is rejected at validation and no fee is charged.

Run on this PR merged onto current release/v0.10.8:

  • types, lint, check:files and dead code pass;
  • bun run test: 3038 pass, 0 fail;
  • coverage floor met (functions 95.54%, lines 96.13%);
  • deployment-status-cache and service-url tests: 27/27.

Merge order: a merge-tree check against the open PRs finds one textual conflict, with #755 (draft) in runtime-rpc.ts, as noted above. #733, #762, #763, #766 and #768 merge cleanly on top of this.

@derek-flossman
derek-flossman merged commit 4296f6b into release/v0.10.8 Oct 26, 2026
5 checks passed
@derek-flossman

Copy link
Copy Markdown
Collaborator

Merged by the repo owner via the release-review bypass; no second review. Head 758bda5 was up to date with release/v0.10.8, with fast-checks and pr-guards green.

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.

2 participants