Skip to content

feat: bundler_provider flag (Alchemy default) + Rundler-portable send path#664

Merged
chrisli30 merged 6 commits into
stagingfrom
fix/erc4337-alchemy-bundler-portability
Jul 21, 2026
Merged

feat: bundler_provider flag (Alchemy default) + Rundler-portable send path#664
chrisli30 merged 6 commits into
stagingfrom
fix/erc4337-alchemy-bundler-portability

Conversation

@chrisli30

Copy link
Copy Markdown
Member
  • fix: make erc4337 send path portable across bundlers (Alchemy Rundler)

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.

  • feat: bundler_provider flag (alchemy default) with dedicated Alchemy 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.

Chris Li 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.

Copilot AI 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.

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 (alchemy default) + alchemy_api_key, and adds ActiveBundlerURL() / BundlerConfigured() to resolve bundler endpoints deterministically per chain.
  • Makes eth_simulateUserOperation best-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 chainId is 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.
@chrisli30
chrisli30 merged commit b061b1c into staging Jul 21, 2026
18 checks passed
@chrisli30
chrisli30 deleted the fix/erc4337-alchemy-bundler-portability branch July 21, 2026 04:50
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