Skip to main content

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

ToolVerifyNotes
Dockerdocker --versionRuns Nitro devnode and cargo-stylus WASM compilation
Rust + WASM targetrustup target list --installed | grep wasm
cargo-styluscargo stylus --version0.10.0 or later
Foundry (cast / forge)cast --version
Node.jsnode --version22.x for the Alto bundler
pnpmpnpm --versionFor Alto build

You will also clone four external repositories under a shared tools directory (referred to below as ~/Developer/tools/dlt):

  • OffchainLabs/nitro-devnode
  • pimlicolabs/alto
  • eth-infinitism/account-abstraction (tag v0.7.0)
  • zerodev-labs/kernel (branch dev)

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:

  1. Deploy PQValidatorModule with the Stylus verifier address in the constructor.
  2. Install on a Kernel account: Kernel.installModule(TYPE_VALIDATOR=1, pqValidator, initData) where initData is the raw 1,952-byte ML-DSA-65 public key. Gas: ~1.57M.
  3. Grant selector access: Kernel.grantAccess(pqValidationId, execute.selector, true). Gas: ~346K.
  4. Encode the PQ nonce: 0x0001{20-byte validator address}0000. This tells Kernel which non-root validator to route to.
  5. Build a UserOp, sign userOpHash with the ML-DSA private key, attach the 3,309-byte signature, and submit via eth_sendUserOperation with verificationGasLimit >= 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:

  1. Kernel implementation (constructor: EntryPoint address) — ~4.5M gas.
  2. KernelFactory (constructor: Kernel impl) — ~800K gas.
  3. FactoryStaker (constructor: owner) — ~600K gas.
  4. ECDSAValidator — ~400K gas.
  5. Staker approves factory and stakes with the EntryPoint (1 ETH, 86,400s unstake delay).

Troubleshooting

Base stack

  • eth_supportedEntryPoints returns empty — Alto isn't pointed at your devnode. Confirm --chain-type arbitrum and --safe-mode false on the Alto command line.
  • Devnode container won't start — leftover container or port collision on 8547. Re-running dev-stack.sh cleans this; otherwise stop containers manually.
  • cargo stylus check fails — Docker is not running or the wasm32-unknown-unknown toolchain target is missing.

Kernel-specific

  • OutOfGas in validateUserOpverificationGasLimit is too low. ML-DSA verification plus Kernel routing requires at least 2,000,000.
  • InvalidValidator — the PQ nonce is encoded incorrectly. Double-check the 0x0001 prefix 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 run cannot trace Stylus transactions (OpcodeNotFound). Use cast call for on-chain simulation; use forge test --fork-url for 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.