Local dev guide
By the end of this guide, you will have the full PQ smart-account stack running locally: a Nitro devnode, the Stylus verifier deployed, the Solidity validator module installed on a Kernel v3 account, the Alto bundler accepting UserOps, and a signed ML-DSA UserOp successfully executing through the EntryPoint.
This guide is for integrators who want to bring up the full stack before deploying to a public testnet.
Prerequisites
| Tool | Verify | Notes |
|---|---|---|
| Docker | docker --version | Runs Nitro devnode and cargo-stylus WASM compilation |
| Rust + WASM target | rustup target list --installed | grep wasm | |
| cargo-stylus | cargo stylus --version | 0.10.0 or later |
| Foundry (cast / forge) | cast --version | |
| Node.js | node --version | 22.x for the Alto bundler |
| pnpm | pnpm --version | For Alto build |
You will also clone four external repositories under a shared tools directory (referred to below as ~/Developer/tools/dlt):
OffchainLabs/nitro-devnodepimlicolabs/altoeth-infinitism/account-abstraction(tagv0.7.0)zerodev-labs/kernel(branchdev)
One-time setup
Nitro devnode
cd ~/Developer/tools/dlt
git clone https://github.com/OffchainLabs/nitro-devnode.git
First run pulls the Docker image (~1 GB). Subsequent starts are instant.
Alto bundler
cd ~/Developer/tools/dlt
git clone --depth 1 https://github.com/pimlicolabs/alto.git \
&& cd alto && pnpm install && pnpm run build:all
EntryPoint v0.7
cd ~/Developer/tools/dlt
git clone --depth 1 --branch v0.7.0 https://github.com/eth-infinitism/account-abstraction.git
cd account-abstraction && npm install --legacy-peer-deps && npx hardhat compile
Extract bytecodes for devnode genesis injection:
node -e "const a = require('./artifacts/contracts/core/EntryPoint.sol/EntryPoint.json'); \
process.stdout.write(a.bytecode)" \
> ~/Developer/tools/dlt/entrypoint_v07_bytecode.txt
node -e "const a = require('./artifacts/contracts/samples/SimpleAccountFactory.sol/SimpleAccountFactory.json'); \
process.stdout.write(a.bytecode)" \
> ~/Developer/tools/dlt/simple_account_factory_bytecode.txt
Kernel v3
git clone https://github.com/zerodev-labs/kernel.git ~/Developer/tools/dlt/kernel
cd ~/Developer/tools/dlt/kernel && git checkout dev && forge install
Kernel must compile with via-ir = true to stay under the 24 KB EIP-170 limit. Default builds produce roughly 29 KB. Use the deploy profile:
FOUNDRY_PROFILE=deploy forge build
Daily workflow (automated)
One command starts the base stack — devnode, EntryPoint/Factory deploys, executor funding, and Alto bundler:
./scripts/dev-stack.sh
Ctrl-C to stop. Safe to re-run; it cleans stale containers and processes automatically.
In a second terminal, load the freshly deployed addresses:
source .env.local
This sets LOCAL_RPC, BUNDLER_RPC, CHAIN_ID, ENTRYPOINT, SIMPLE_ACCOUNT_FACTORY, and DEV_PRIVATE_KEY. EntryPoint and factory addresses change on every restart (the devnode is ephemeral). Export your own deploy addresses explicitly:
export STYLUS_VERIFIER=<deployed address>
export PQ_VALIDATOR=<deployed address>
export KERNEL_ACCOUNT=<deployed address>
Daily workflow (manual)
Step 1: Stylus verifier
# Unit tests
cargo test --package pq-validator
# Validate WASM constraints (needs Docker)
cargo stylus check --manifest-path pq-validator/Cargo.toml
# Deploy to devnode
cargo stylus deploy \
--endpoint='http://127.0.0.1:8547' \
--private-key=<DEV_PK> \
--no-verify \
--manifest-path pq-validator/Cargo.toml
Step 2: Solidity validator module
# Build and test
forge build --root evm
forge test --root evm -vvv
# Run tests against a live devnode (cross-runtime calls hit Stylus)
forge test --root evm --fork-url http://127.0.0.1:8547 -vvv
# Deploy
forge create evm/src/PQValidatorModule.sol:PQValidatorModule \
--rpc-url http://127.0.0.1:8547 \
--private-key $DEV_PK \
--constructor-args $STYLUS_VERIFIER
# Smoke test
cast call $PQ_VALIDATOR "isModuleType(uint256)(bool)" 1 \
--rpc-url http://127.0.0.1:8547
Step 3: PQ validator integration
After the base stack is up:
- Deploy
PQValidatorModulewith the Stylus verifier address in the constructor. - Install on a Kernel account:
Kernel.installModule(TYPE_VALIDATOR=1, pqValidator, initData)whereinitDatais the raw 1,952-byte ML-DSA-65 public key. Gas: ~1.57M. - Grant selector access:
Kernel.grantAccess(pqValidationId, execute.selector, true). Gas: ~346K. - Encode the PQ nonce:
0x0001{20-byte validator address}0000. This tells Kernel which non-root validator to route to. - Build a UserOp, sign
userOpHashwith the ML-DSA private key, attach the 3,309-byte signature, and submit viaeth_sendUserOperationwithverificationGasLimit >= 2_000_000.
Step 4: Verify end-to-end
# Bundler health check
curl -s http://127.0.0.1:4337 -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_supportedEntryPoints","params":[],"id":1}'
# Send a signed UserOp
curl -s http://127.0.0.1:4337 -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_sendUserOperation","params":[<SIGNED_USEROP_JSON>,"'$ENTRYPOINT'"],"id":1}'
# Fetch the receipt
curl -s http://127.0.0.1:4337 -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getUserOperationReceipt","params":["<USEROP_HASH>"],"id":1}'
Kernel v3 deployment order
Kernel deploys must use the deploy profile, and order matters. The end-to-end script handles this; the manual sequence is:
Kernelimplementation (constructor: EntryPoint address) — ~4.5M gas.KernelFactory(constructor: Kernel impl) — ~800K gas.FactoryStaker(constructor: owner) — ~600K gas.ECDSAValidator— ~400K gas.- Staker approves factory and stakes with the EntryPoint (1 ETH, 86,400s unstake delay).
Troubleshooting
Base stack
eth_supportedEntryPointsreturns empty — Alto isn't pointed at your devnode. Confirm--chain-type arbitrumand--safe-mode falseon the Alto command line.- Devnode container won't start — leftover container or port collision on 8547. Re-running
dev-stack.shcleans this; otherwise stop containers manually. cargo stylus checkfails — Docker is not running or thewasm32-unknown-unknowntoolchain target is missing.
Kernel-specific
OutOfGasinvalidateUserOp—verificationGasLimitis too low. ML-DSA verification plus Kernel routing requires at least 2,000,000.InvalidValidator— the PQ nonce is encoded incorrectly. Double-check the0x0001prefix and the 20-byte validator address are placed correctly in the 32-byte nonce.- Kernel deploys at 29 KB — you forgot
FOUNDRY_PROFILE=deploy. Rebuild with via-ir.
Debugging
cast runcannot trace Stylus transactions (OpcodeNotFound). Usecast callfor on-chain simulation; useforge test --fork-urlfor the EVM side.- Bundler simulation errors usually surface as JSON-RPC responses with an
AA##code. The relevant codes are in the EIP-4337 spec and the Alto README.
Production considerations
The local devnode does not exercise the real cost or compatibility constraints of mainnet:
- Bundler simulation of Stylus calls — ERC-7562 storage access rules need to be validated against any bundler you intend to use in production. Behavior may differ between Alto, Pimlico hosted, Alchemy, Stackup, and others.
- L1 data cost — devnode L1 data is free. Real L1 calldata dominates per-transaction cost on Arbitrum. See Gas and cost model.
- Stylus activation lifecycle — Stylus contracts auto-deactivate after 365 days or after ArbOS upgrades. Schedule reactivation explicitly.