feat: bundler_provider flag (Alchemy default) + Rundler-portable send path#664
Merged
Merged
Conversation
added 2 commits
July 20, 2026 13:15
Routing UserOps at a third-party bundler (Alchemy Rundler) instead of our self-hosted Voltaire surfaced two client-side incompatibilities. Neither is a chain/account/paymaster issue — a real Sepolia UserOp (SimpleAccount + our VerifyingPaymaster, v0.6 EntryPoint) now lands through both Voltaire and Alchemy. 1. eth_simulateUserOperation is a Voltaire/Candide extension. Rundler returns -32600/-32601 "Unsupported method". The call was an early convenience check; make it best-effort (skip on unsupported-method, keep the hard failure for a genuine sim error on bundlers that do implement it). sendUserOpCore still runs eth_estimateUserOperationGas, which simulates against the EntryPoint. 2. The wrapped reimbursement op (executeBatchWithValues) skipped gas estimation and shipped the 1M DEFAULT_VERIFICATION_GAS_LIMIT. Rundler enforces a verification-gas-limit-efficiency floor (>=20%); 1M vs ~30K actual ≈ 3% → reject. Verification gas is independent of the execution calldata, so reuse the accurate value from the unwrapped estimate (+15% buffer) instead of the blind default. Call gas keeps the padded default (wrapped exec can't be simulated). This also tightens the Voltaire path (accurate ~30K vs a blind 1M). Also fix TestUserOpETHWithdrawal_Sepolia, which had rotted after chain-decoupling (G5): chain_id must be on the node config (camelCase chainId), not settings, for a direct RunNodeImmediately call. Shrink the fixed withdrawal to 0.00001 ETH to conserve testnet ETH across runs.
…endpoint Adds an explicit, per-chain switch between the Alchemy and self-hosted (Voltaire) bundlers, defaulting to Alchemy. The Alchemy path derives its endpoint from a dedicated alchemy_api_key + a chainId→subdomain map and NEVER reads bundler_url; the self_hosted path uses bundler_url. A chain configured for alchemy with no key (or an unmapped chain) is a hard error at send time — fail closed, no silent fallback. - SmartWalletConfig gains BundlerProvider + AlchemyAPIKey; ActiveBundlerURL() resolves the endpoint (default alchemy), BundlerConfigured() reports whether a bundler resolves (replaces the old `bundler_url != ""` connectivity-only guard). - All 3 bundler-client construction sites (builder.go, vm_runner_contract_write.go, vm_contract_write_waiting.go) resolve via ActiveBundlerURL(). - Plumbed through config conversions (top-level + per-chain) and worker config. - test.example.yaml / gateway.example.yaml document the fields; testutil pins self_hosted so the suite keeps using Voltaire deterministically. - Unit tests cover URL derivation, default-to-alchemy, and every hard-error path. Validated end-to-end on Sepolia: a real UserOp lands via both providers (self_hosted through bundler_url, alchemy purely from alchemy_api_key). ROLLOUT (guardian_ruleset gotcha — config ships separately from code): before this reaches an environment, every wallet-op chain in avs-infra must set alchemy_api_key OR bundler_provider: self_hosted, else the gateway hard-errors on send.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the ERC-4337 “send UserOp” path and configuration model to support routing through either the self-hosted Voltaire bundler or Alchemy’s Rundler, defaulting to Alchemy, while keeping the send flow portable across bundlers with different RPC method support and gas-limit policies.
Changes:
- Introduces
bundler_provider(alchemydefault) +alchemy_api_key, and addsActiveBundlerURL()/BundlerConfigured()to resolve bundler endpoints deterministically per chain. - Makes
eth_simulateUserOperationbest-effort (skip on unsupported-method responses) and improves wrapped reimbursement operations by reusing unwrapped verification gas estimates (+ buffer) to satisfy Rundler’s efficiency floor. - Fixes and tightens Sepolia UserOp withdrawal testing by correcting where
chainIdis supplied and reducing the withdrawal amount.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| worker/config.go | Extends worker config to carry bundler provider + Alchemy key into SmartWalletConfig. |
| pkg/erc4337/preset/builder.go | Uses ActiveBundlerURL(), makes simulate non-fatal on unsupported method, and improves wrapped-op verification gas handling. |
| core/testutil/utils.go | Pins test configs to self_hosted bundler provider to keep the suite deterministic. |
| core/taskengine/vm_runner_contract_write.go | Resolves bundler endpoint via ActiveBundlerURL() for diagnostics client creation. |
| core/taskengine/vm_contract_write_waiting.go | Resolves bundler endpoint via ActiveBundlerURL() when waiting for UserOp confirmation. |
| core/taskengine/userops_withdraw_test.go | Fixes Sepolia withdrawal test chain configuration and reduces ETH withdrawal amount. |
| core/config/config.go | Adds bundler provider/key fields, endpoint resolution helpers, and updates paymaster probing guard to use BundlerConfigured(). |
| core/config/bundler_provider_test.go | Adds unit tests for bundler URL derivation and configured/unconfigured behavior. |
| config/test.example.yaml | Documents new bundler provider + Alchemy key fields (tests default to self-hosted). |
| config/gateway.example.yaml | Documents default-to-alchemy behavior and fail-closed requirements for wallet-op chains. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The provider defaults to alchemy and hard-errors without a key, so example
templates that only set bundler_url would fail when copied. Add
bundler_provider + alchemy_api_key to every wallet-op block, pinned to
self_hosted (works out-of-box with the existing Voltaire ${*_BUNDLER_URL})
with a comment noting the code default is alchemy:
- gateway.example.yaml: the two per-chain smart_wallet blocks (top-level was
already migrated; also switched it from alchemy→self_hosted for consistency).
- worker-sepolia.example.yaml / worker-base-sepolia.example.yaml: top-level.
Alchemy's bnb-mainnet bundler exposes v0.6, so the code can route BNB to Alchemy once BNB becomes a wallet-op chain. NOTE: BNB stays bundler_provider: self_hosted in avs-infra for now — it's connectivity-only with a placeholder testnet paymaster on BNB mainnet, so making BundlerConfigured() true would fail the fatal startup paymaster probe. Flip BNB's config to alchemy only after a real BNB paymaster is deployed + funded.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Routing UserOps at a third-party bundler (Alchemy Rundler) instead of our
self-hosted Voltaire surfaced two client-side incompatibilities. Neither is a
chain/account/paymaster issue — a real Sepolia UserOp (SimpleAccount + our
VerifyingPaymaster, v0.6 EntryPoint) now lands through both Voltaire and Alchemy.
eth_simulateUserOperation is a Voltaire/Candide extension. Rundler returns
-32600/-32601 "Unsupported method". The call was an early convenience check;
make it best-effort (skip on unsupported-method, keep the hard failure for a
genuine sim error on bundlers that do implement it). sendUserOpCore still runs
eth_estimateUserOperationGas, which simulates against the EntryPoint.
The wrapped reimbursement op (executeBatchWithValues) skipped gas estimation
and shipped the 1M DEFAULT_VERIFICATION_GAS_LIMIT. Rundler enforces a
verification-gas-limit-efficiency floor (>=20%); 1M vs ~30K actual ≈ 3% → reject.
Verification gas is independent of the execution calldata, so reuse the accurate
value from the unwrapped estimate (+15% buffer) instead of the blind default.
Call gas keeps the padded default (wrapped exec can't be simulated). This also
tightens the Voltaire path (accurate ~30K vs a blind 1M).
Also fix TestUserOpETHWithdrawal_Sepolia, which had rotted after chain-decoupling
(G5): chain_id must be on the node config (camelCase chainId), not settings, for a
direct RunNodeImmediately call. Shrink the fixed withdrawal to 0.00001 ETH to
conserve testnet ETH across runs.
Adds an explicit, per-chain switch between the Alchemy and self-hosted (Voltaire)
bundlers, defaulting to Alchemy. The Alchemy path derives its endpoint from a
dedicated alchemy_api_key + a chainId→subdomain map and NEVER reads bundler_url;
the self_hosted path uses bundler_url. A chain configured for alchemy with no key
(or an unmapped chain) is a hard error at send time — fail closed, no silent
fallback.
resolves the endpoint (default alchemy), BundlerConfigured() reports whether a
bundler resolves (replaces the old
bundler_url != ""connectivity-only guard).vm_contract_write_waiting.go) resolve via ActiveBundlerURL().
self_hosted so the suite keeps using Voltaire deterministically.
Validated end-to-end on Sepolia: a real UserOp lands via both providers
(self_hosted through bundler_url, alchemy purely from alchemy_api_key).
ROLLOUT (guardian_ruleset gotcha — config ships separately from code): before
this reaches an environment, every wallet-op chain in avs-infra must set
alchemy_api_key OR bundler_provider: self_hosted, else the gateway hard-errors
on send.