Skip to content

feat(pxe): speculatively sync predicted contract calls - #25126

Open
nchamo wants to merge 3 commits into
merge-train/fairiesfrom
nchamo/rpc-optimizations-2
Open

feat(pxe): speculatively sync predicted contract calls#25126
nchamo wants to merge 3 commits into
merge-train/fairiesfrom
nchamo/rpc-optimizations-2

Conversation

@nchamo

@nchamo nchamo commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Why we are doing this

The last few weeks of PXE work have been about cutting RPC calls and round trips (#25033, #25074, #25076, #25089, #25116). Measuring what is left on the transfers benchmark, contract sync is now the single biggest source: 67% of the calls and 80% of the round trips.

It is also the hardest part to run in parallel, because a job's contract dependencies are only discovered as execution reaches them. A transfer that touches an account, a token and an FPC syncs them one after another, so each contract's round trips wait on every earlier contract's.

Our fix

Predict the contracts a job is about to need from its past runs, so their syncs can all start at once.

  • A call graph over contract functions — who calls whom — is learned from committed jobs. Calls are keyed per function, not per contract, since different functions of a contract call different contracts: a job in which a caller calls a callee adds a point of confidence to that call (capped at 5), a job in which the caller calls other functions but not that one subtracts a point (dropping it at zero), and a job in which it makes no calls changes nothing.
  • When a contract's sync is requested to invoke a function, every callee of that function at confidence 2 or above starts its contract's sync speculatively, and each of those fires its own predictions in turn — the whole predicted call tree syncs in parallel with the requested contract.
  • An entry function whose flows vary (e.g. an account entrypoint) rarely reaches the threshold for any single callee, so it predicts little on its own; but the moment execution commits to one callee, that callee's whole subtree fires.

Best case, all the syncs collapse into a single round trip and every one of them was needed. Worst case, we spend extra calls on contracts the job never uses, but they ride the same round trip, so they cost no extra latency. A speculative sync that fails is dropped from the cache, so it cannot fail a job that did not need it, and the next request retries it from scratch.

Configuration

This is experimental, so it is off by default (PXE_CONCURRENT_CONTRACT_SYNC_ENABLED). The prediction is only as good as the workload is repetitive: a general purpose wallet running many different flows may see little from it, while an embedded wallet doing the same operation over and over should benefit.

Metrics

BENCHMARK_CONFIG=key_flows on the transfers bench. Each of the four flows runs three times so the predictor reaches steady state. Both sides are the same build toggled by the env var, so the delta is the feature and nothing else (the disabled path returns before any bookkeeping).

Round trips, disabled → enabled, flows in execution order:

flow run 1 run 2 run 3
sponsored_fpc, 0 recursions 23 → 23 20 → 16 19 → 16
sponsored_fpc, 1 recursion 20 → 16 19 → 17 19 → 16
private_fpc, 0 recursions 22 → 18 20 → 16 19 → 15
private_fpc, 1 recursion 20 → 17 20 → 17 20 → 17
total 85 → 74 (-13%) 79 → 66 (-16%) 77 → 64 (-17%)

Once the dependencies are learned the flows cost 17% fewer round trips. Only same run columns are comparable, because the disabled side improves across runs too: a contract whose notes are not in the store yet skips its nullifier lookup entirely, so its class id read travels alone, and once an earlier run has populated the store the two reads share a round trip.

Calls are learned per function, not per flow, and all four flows enter through the same account entrypoint and share the token. Only the very first flow's first run is genuinely cold, and it is reported unchanged because nothing is predicted there; by the second run the threshold is already met, since the change sending transaction between runs is another job making the same calls.

Call counts are identical on both sides in every run (144 on the first, 142 afterwards), so every prediction that fired was a hit and this workload wasted nothing.

@nchamo nchamo self-assigned this Aug 6, 2026
@nchamo nchamo added ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure labels Aug 6, 2026
@nchamo
nchamo marked this pull request as ready for review August 6, 2026 13:57
@nchamo
nchamo requested review from Thunkar and nventuro August 6, 2026 14:12
utilityExecutor,
scope,
),
async ensureContractSynced({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Decided to change the params with an obj param, so that it was easier to understand what each param was

* Waits until every sync the job started has settled, so all its staged writes land before the job's stores
* commit or discard. Never rejects: sync failures are surfaced by the requests that await them, not here.
*/
async settle(jobId: JobId): Promise<void> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Had to add this to the store's interface, so that we could wait for all syncs to be settled (either worked or failed). More about it on the JobCoordinator

@nventuro nventuro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(these are human comments, I've not yet gone over the thing with ai)

Comment thread yarn-project/pxe/src/config/index.ts Outdated
Comment thread yarn-project/pxe/src/config/index.ts Outdated
Comment thread yarn-project/pxe/src/contract/contract_call_dependencies.ts Outdated
Comment thread yarn-project/pxe/src/contract/contract_call_dependencies.ts Outdated
Comment thread yarn-project/pxe/src/contract/contract_call_dependencies.ts Outdated
Comment thread yarn-project/pxe/src/contract/contract_call_dependencies.ts Outdated
Comment thread yarn-project/pxe/src/contract/contract_call_dependencies.ts Outdated
Comment thread yarn-project/pxe/src/contract/contract_call_dependencies.test.ts Outdated
contractClassService,
noteStore,
createLogger('test:contract-sync'),
true, // concurrentContractSyncEnabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is full of these comments. What if we just make this an object? Raw booleans are unparseable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, replaced the bool with ContractSyncConfig so that it's easier to read. Could replace all the args with an object if you prefer

Comment thread yarn-project/pxe/src/contract/contract_sync_service.ts Outdated
@nchamo
nchamo requested a review from nventuro August 9, 2026 22:55
@nchamo nchamo changed the title feat(pxe): speculatively sync predicted contract dependencies feat(pxe): speculatively sync predicted contract calls Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-to-v5-next ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants