# POPKins Local Devnet: OP Stack for Local Development ## Product Requirements Document **Status:** Draft **Last Updated:** December 2025 **Audience:** Developers wanting to run OP Stack locally without external dependencies --- ## 1. Product Summary POPKins Local Devnet is a zero-dependency local OP Stack development environment. It enables developers to run a complete OP Stack chain on their local machine without connecting to Ethereum, Celestia, or POPSigner Cloud. ### What It Does - Generates a ready-to-run local OP Stack environment - Includes pre-deployed L1 contracts on Anvil (mock Ethereum) - Includes mock Celestia DA layer with full op-alt-da integration - Includes POPSigner Lite for local transaction signing - Ships as a downloadable artifact bundle with `docker compose up` experience ### Why It Exists | Problem | Solution | |---------|----------| | Real testnet deployments are slow and cost gas | Anvil is instant and free | | Celestia DA requires running a light node | Mock Celestia is in-memory | | POPSigner Cloud requires API keys and internet | POPSigner Lite works offline | | Complex setup discourages new developers | One command: `docker compose up` | ### The Funnel Local Devnet is the **entry point** to POPSigner's ecosystem: ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Local Devnet │ ──▶ │ Sepolia/Testnet│ ──▶ │ Mainnet │ │ (free, local) │ │ (real chains) │ │ (production) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ Same flags Same flags Same flags Same binaries Same binaries Same binaries Mock backends Real backends Real backends ``` Developers learn POPSigner's workflow locally, then seamlessly upgrade to real infrastructure. --- ## 2. Target User Persona **Primary Persona:** Application developers building on OP Stack rollups. This includes: - Developers building L2 dApps - Teams evaluating OP Stack + Celestia DA - Engineers learning rollup architecture - CI/CD pipelines needing ephemeral chains **These users:** - Want to iterate quickly without testnet delays - Don't want to manage infrastructure or keys - Prefer `docker compose up` simplicity - Value offline/airgapped development capability --- ## 3. Design Principles | # | Principle | Description | |---|-----------|-------------| | 1 | Zero external dependencies | Works completely offline | | 2 | Same binaries as production | op-node, op-geth, op-batcher, op-proposer, op-alt-da are all real | | 3 | State persistence | Developers can stop/resume their devnet | | 4 | Instant feedback | Anvil automines, mock Celestia has no finality delay | | 5 | One-command startup | `docker compose up` and you're running | | 6 | Clear upgrade path | Same config patterns work on testnet/mainnet | --- ## 4. Architecture ### 4.1 Component Overview ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ LOCAL DEVNET │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │ │ │ op-geth │◀───▶│ op-node │◀───▶│ op-alt-da │ │ │ │ (L2) │ │ │ │ (real) │ │ │ └─────────────┘ └──────┬──────┘ └──────────────┬──────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────────┐ │ │ │ Anvil │ │ mock-celestia │ │ │ │ (mock L1) │ │ (mock DA) │ │ │ └─────────────┘ └─────────────────┘ │ │ ▲ │ │ │ │ │ ┌─────────────┐ ┌──────┴──────┐ │ │ │ op-proposer │────▶│popsigner-lite│◀────┌─────────────┐ │ │ └─────────────┘ └─────────────┘ │ op-batcher │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### 4.2 Component Details | Component | Image | Purpose | Backend | |-----------|-------|---------|---------| | **Anvil** | `ghcr.io/foundry-rs/foundry` | Mock L1 Ethereum | In-memory + state dump | | **mock-celestia** | `ghcr.io/bidon15/mock-celestia` | Mock Celestia DA layer | In-memory + state dump | | **op-alt-da** | `ghcr.io/celestiaorg/op-alt-da` | Real DA server | → mock-celestia | | **popsigner-lite** | `ghcr.io/bidon15/popsigner-lite` | Local signing service | Anvil's known keys | | **op-geth** | `us-docker.pkg.dev/.../op-geth` | L2 execution layer | - | | **op-node** | `us-docker.pkg.dev/.../op-node` | L2 derivation | → Anvil + op-alt-da | | **op-batcher** | `us-docker.pkg.dev/.../op-batcher` | Batch submission | → Anvil + op-alt-da + popsigner-lite | | **op-proposer** | `us-docker.pkg.dev/.../op-proposer` | State root submission | → Anvil + popsigner-lite | ### 4.3 Key Assignments Local devnet uses Anvil's well-known deterministic keys: | Role | Address | Private Key | |------|---------|-------------| | Deployer | `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` | `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` | | Batcher | `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` | `0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d` | | Proposer | `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` | `0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a` | | Admin | `0x90F79bf6EB2c4f870365E785982E1f101E93b906` | `0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6` | **Note:** These keys are publicly known and ONLY for local development. Never use them on real networks. --- ## 5. User Journey ### Step 1: Configure Local Devnet (Dashboard) User visits POPKins dashboard and selects: ``` ┌─────────────────────────────────────────────────────────────────┐ │ CREATE NEW CHAIN │ │ │ │ Target Environment: │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ ○ Sepolia │ │ ○ Mainnet │ │ ● Local Devnet (Anvil) │ │ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ │ │ │ Chain Name: my-local-rollup │ │ Chain ID: 42069 (auto-generated, editable) │ │ │ │ [Generate Devnet Bundle] │ │ │ │ ⚡ No funding required - runs entirely locally │ └─────────────────────────────────────────────────────────────────┘ ``` ### Step 2: Backend Generates Bundle POPKins backend: 1. Spins up ephemeral Anvil instance 2. Deploys OP Stack contracts using Anvil account[0] 3. Generates L2 genesis from deployed contracts 4. Dumps Anvil state via `anvil_dumpState` 5. Packages everything into downloadable bundle **Generation time:** ~30-60 seconds ### Step 3: Download Bundle ``` ┌─────────────────────────────────────────────────────────────────┐ │ LOCAL DEVNET READY ✅ │ │ │ │ Chain: my-local-rollup │ │ Chain ID: 42069 │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 📦 DOWNLOAD DEVNET BUNDLE │ │ │ │ │ │ │ │ Includes: │ │ │ │ • Pre-deployed L1 contracts (Anvil state) │ │ │ │ • L2 genesis file │ │ │ │ • Docker Compose configuration │ │ │ │ • All config files │ │ │ │ │ │ │ │ [Download .tar.gz (15 MB)] │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ Quick Start: │ │ $ tar -xzf my-local-rollup-devnet.tar.gz │ │ $ cd my-local-rollup-devnet │ │ $ docker compose up │ │ │ │ L2 RPC will be available at http://localhost:8545 │ └─────────────────────────────────────────────────────────────────┘ ``` ### Step 4: Run Locally ```bash # Extract bundle tar -xzf my-local-rollup-devnet.tar.gz cd my-local-rollup-devnet # Start everything docker compose up -d # Check status docker compose ps # View logs docker compose logs -f op-node ``` ### Step 5: Develop Developer has a fully functional OP Stack: | Endpoint | URL | |----------|-----| | L2 JSON-RPC | `http://localhost:8545` | | L2 WebSocket | `ws://localhost:8546` | | L1 (Anvil) | `http://localhost:8547` | | op-node RPC | `http://localhost:9545` | --- ## 6. Artifact Bundle Structure ``` my-local-rollup-devnet/ ├── docker-compose.yml # Ready-to-run configuration ├── .env # Pre-filled (not a template!) │ ├── state/ │ ├── anvil-state.json # L1 state with deployed contracts │ └── celestia-state.json # DA state (initially empty) │ ├── genesis/ │ └── genesis.json # L2 genesis (~50MB) │ ├── config/ │ ├── rollup.json # Rollup configuration │ ├── addresses.json # All deployed contract addresses │ └── jwt.txt # JWT secret for op-geth auth │ ├── scripts/ │ ├── start.sh # docker compose up -d │ ├── stop.sh # docker compose down (preserves state) │ ├── reset.sh # Reset to initial state │ └── logs.sh # docker compose logs -f │ └── README.md # Quick start guide ``` --- ## 7. Docker Compose Configuration ```yaml version: "3.8" x-logging: &logging driver: json-file options: max-size: "10m" max-file: "3" services: # ========================================================================= # L1 - Anvil (Mock Ethereum) # ========================================================================= anvil: image: ghcr.io/foundry-rs/foundry:latest restart: unless-stopped logging: *logging command: > anvil --host 0.0.0.0 --port 8545 --load-state /state/anvil-state.json --state /state/anvil-state.json --accounts 10 --balance 10000 volumes: - ./state:/state ports: - "8547:8545" # L1 RPC (different port to avoid conflict with L2) # ========================================================================= # DA Layer - Mock Celestia # ========================================================================= mock-celestia: image: ghcr.io/bidon15/mock-celestia:latest restart: unless-stopped logging: *logging command: > --load-state /state/celestia-state.json --dump-state-path /state/celestia-state.json --json-rpc-port 26658 --grpc-port 9090 volumes: - ./state:/state ports: - "26658:26658" # JSON-RPC (bridge API) - "9090:9090" # gRPC (core API) # ========================================================================= # DA Server - Real op-alt-da pointing to mock Celestia # ========================================================================= op-alt-da: image: ghcr.io/celestiaorg/op-alt-da:latest restart: unless-stopped logging: *logging environment: - OP_ALTDA_ADDR=0.0.0.0 - OP_ALTDA_PORT=3100 - OP_ALTDA_CELESTIA_BRIDGE_ADDR=http://mock-celestia:26658 - OP_ALTDA_CELESTIA_CORE_GRPC_ADDR=mock-celestia:9090 - OP_ALTDA_CELESTIA_CORE_GRPC_TLS_ENABLED=false - OP_ALTDA_CELESTIA_NAMESPACE=${CELESTIA_NAMESPACE} ports: - "3100:3100" depends_on: - mock-celestia # ========================================================================= # Signing - POPSigner Lite (local, offline) # ========================================================================= popsigner-lite: image: ghcr.io/bidon15/popsigner-lite:latest restart: unless-stopped logging: *logging environment: # Anvil's well-known private keys (not sensitive - these are public!) - PRIVATE_KEYS=${ANVIL_PRIVATE_KEYS} ports: - "8080:8080" # ========================================================================= # L2 Execution - op-geth # ========================================================================= op-geth: image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101408.0 restart: unless-stopped logging: *logging command: - --datadir=/data - --http - --http.addr=0.0.0.0 - --http.port=8545 - --http.api=eth,net,web3,debug,txpool - --ws - --ws.addr=0.0.0.0 - --ws.port=8546 - --ws.api=eth,net,web3 - --authrpc.addr=0.0.0.0 - --authrpc.port=8551 - --authrpc.jwtsecret=/config/jwt.txt - --gcmode=archive - --syncmode=full - --rollup.sequencerhttp=http://op-node:8545 volumes: - geth-data:/data - ./genesis/genesis.json:/genesis.json:ro - ./config:/config:ro ports: - "8545:8545" # L2 JSON-RPC - "8546:8546" # L2 WebSocket healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8545"] interval: 10s timeout: 5s retries: 5 # ========================================================================= # L2 Derivation - op-node # ========================================================================= op-node: image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-node:v1.9.0 restart: unless-stopped logging: *logging command: - op-node - --l1=http://anvil:8545 - --l2=http://op-geth:8551 - --l2.jwt-secret=/config/jwt.txt - --rollup.config=/config/rollup.json - --rpc.addr=0.0.0.0 - --rpc.port=8545 - --p2p.disable - --altda.enabled=true - --altda.da-server=http://op-alt-da:3100 volumes: - ./config:/config:ro ports: - "9545:8545" depends_on: op-geth: condition: service_healthy op-alt-da: condition: service_started anvil: condition: service_started # ========================================================================= # Batcher - op-batcher (signs via popsigner-lite) # ========================================================================= op-batcher: image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-batcher:v1.9.0 restart: unless-stopped logging: *logging command: - op-batcher - --l1-eth-rpc=http://anvil:8545 - --l2-eth-rpc=http://op-geth:8545 - --rollup-rpc=http://op-node:8545 - --signer.endpoint=http://popsigner-lite:8080 - --signer.address=${BATCHER_ADDRESS} - --altda.enabled=true - --altda.da-server=http://op-alt-da:3100 - --max-channel-duration=1 - --sub-safety-margin=4 depends_on: - op-node - op-geth - op-alt-da - popsigner-lite # ========================================================================= # Proposer - op-proposer (signs via popsigner-lite) # ========================================================================= op-proposer: image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-proposer:v1.9.0 restart: unless-stopped logging: *logging command: - op-proposer - --l1-eth-rpc=http://anvil:8545 - --rollup-rpc=http://op-node:8545 - --l2oo-address=${L2_OUTPUT_ORACLE_ADDRESS} - --signer.endpoint=http://popsigner-lite:8080 - --signer.address=${PROPOSER_ADDRESS} depends_on: - op-node - popsigner-lite volumes: geth-data: networks: default: name: ${CHAIN_NAME}-devnet ``` --- ## 8. Environment File (.env) ```bash # ============================================================================= # LOCAL DEVNET CONFIGURATION # Chain: my-local-rollup # Chain ID: 42069 # ============================================================================= # Chain identity CHAIN_NAME=my-local-rollup CHAIN_ID=42069 # Role addresses (Anvil's deterministic accounts) DEPLOYER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 BATCHER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 PROPOSER_ADDRESS=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC ADMIN_ADDRESS=0x90F79bf6EB2c4f870365E785982E1f101E93b906 # Contract addresses (from deployment) L2_OUTPUT_ORACLE_ADDRESS=0x... # Anvil private keys (comma-separated, loaded by popsigner-lite) ANVIL_PRIVATE_KEYS=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80,0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d,0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a,0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 # Celestia namespace (generated for this devnet) CELESTIA_NAMESPACE=00000000000000000000000000000000000000000000000000000000abcd ``` --- ## 9. New Components to Build ### 9.1 mock-celestia **Purpose:** Mock Celestia node that implements the APIs op-alt-da expects. **Interfaces:** | API | Port | Purpose | |-----|------|---------| | gRPC | 9090 | `BlobService.Submit` - accept blob submissions | | JSON-RPC | 26658 | `blob.Get` - retrieve blobs | | HTTP | 8080 | `/dump`, `/health` - state management | **Features:** - In-memory blob storage - Instant "finality" (no consensus delay) - State dump/load (like Anvil) - No keyring/signature verification needed **Size:** ~500 lines Go ### 9.2 popsigner-lite **Purpose:** Minimal local signing service with Anvil's known keys. **Interface:** | Endpoint | Method | Purpose | |----------|--------|---------| | `/` | POST | JSON-RPC: `eth_signTransaction` | | `/health` | GET | Health check | **Features:** - Pre-loaded with Anvil's 10 deterministic private keys - Same JSON-RPC interface as POPSigner Cloud - No authentication required - No database, no OpenBao **Size:** ~200 lines Go ### 9.3 Anvil State Generator (Backend) **Purpose:** Deploy OP Stack contracts to Anvil and dump state. **Location:** `control-plane/internal/bootstrap/opstack/anvil_deployer.go` **Flow:** ```go func (o *Orchestrator) GenerateLocalDevnet(ctx context.Context, cfg *LocalDevnetConfig) (*DevnetBundle, error) { // 1. Start ephemeral Anvil container anvil, err := startAnvil(ctx) if err != nil { return nil, err } defer anvil.Stop() // 2. Create signer using Anvil's known key deployerKey := anvil.Account(0) signerAdapter := NewAnvilSignerAdapter(deployerKey) // 3. Deploy OP Stack contracts (reuse existing deployer) deployConfig := &DeploymentConfig{ L1RPC: anvil.RPCURL(), L1ChainID: 31337, // Anvil's chain ID ChainName: cfg.ChainName, ChainID: cfg.ChainID, BatcherAddr: anvil.Account(1).Address, ProposerAddr: anvil.Account(2).Address, AdminAddr: anvil.Account(3).Address, } result, err := o.deployer.Deploy(ctx, deployConfig, signerAdapter, nil) if err != nil { return nil, err } // 4. Dump Anvil state stateJSON, err := anvil.DumpState() if err != nil { return nil, err } // 5. Generate docker-compose and bundle return &DevnetBundle{ AnvilState: stateJSON, Genesis: result.Genesis, RollupConfig: result.RollupConfig, Addresses: result.Addresses, DockerCompose: generateLocalCompose(cfg, result), EnvFile: generateEnvFile(cfg, result), }, nil } ``` --- ## 10. API Endpoints ### Create Local Devnet ```http POST /api/v1/deployments Content-Type: application/json { "chainType": "opstack", "targetEnvironment": "local", // NEW: indicates local devnet "name": "my-local-rollup", "l2ChainId": 42069, "config": { // Optional overrides } } ``` ### Response ```json { "id": "01HXYZ...", "status": "generating", "targetEnvironment": "local", "estimatedTime": "45s" } ``` ### Download Bundle ```http GET /api/v1/deployments/{id}/bundle Authorization: Bearer <token> Response: application/gzip Content-Disposition: attachment; filename="my-local-rollup-devnet.tar.gz" ``` --- ## 11. CLI Support ```bash # Create local devnet popctl bootstrap create \ --name my-rollup \ --chain-id 42069 \ --target local # Download bundle popctl bootstrap download <deployment-id> --output ./my-rollup/ # Or one-liner popctl bootstrap create --name my-rollup --target local --download ./my-rollup/ ``` --- ## 12. State Management ### Persistence All state is persisted in the `./state/` directory: | File | Contents | Persistence | |------|----------|-------------| | `anvil-state.json` | L1 accounts, contracts, storage | Auto-saved on shutdown | | `celestia-state.json` | DA blobs | Auto-saved on shutdown | | `geth-data/` | L2 chain data | Docker volume | ### Stop/Resume ```bash # Stop (preserves state) docker compose down # Resume (loads state) docker compose up -d ``` ### Reset ```bash # Reset to initial state ./scripts/reset.sh # (Copies initial state files back, removes geth-data volume) docker compose up -d ``` --- ## 13. Upgrade Path When developers are ready to move from local to testnet: | Local Devnet | → | Sepolia (Real) | |--------------|---|----------------| | `popsigner-lite:8080` | → | `rpc.popsigner.com` | | `anvil:8545` | → | Sepolia RPC | | `mock-celestia` | → | Celestia Mocha | | No auth | → | API key auth | | Instant finality | → | Real block times | **Changes required:** 1. Update `.env` with real endpoints and credentials 2. Re-deploy contracts to Sepolia (via POPKins) 3. Fund deployer address with testnet ETH The docker-compose structure and `--signer.*` flags remain identical. --- ## 14. Success Metrics | Metric | Target | |--------|--------| | Bundle generation time | < 60 seconds | | Bundle download size | < 20 MB (compressed) | | Time to first L2 block | < 30 seconds after `docker compose up` | | Works offline | 100% | | Zero config required | `docker compose up` just works | --- ## 15. Implementation Checklist ### New Components | Task | Priority | Estimated Effort | |------|----------|------------------| | `mock-celestia` service | P0 | 2-3 days | | `popsigner-lite` service | P0 | 1 day | | Anvil state generator | P0 | 2 days | | Docker Compose template (local) | P0 | 1 day | | Bundle generator | P0 | 1 day | ### Dashboard Updates | Task | Priority | Estimated Effort | |------|----------|------------------| | "Local Devnet" option in chain creation | P1 | 0.5 day | | Bundle download UI | P1 | 0.5 day | | Quick start instructions | P1 | 0.5 day | ### CLI Updates | Task | Priority | Estimated Effort | |------|----------|------------------| | `--target local` flag | P2 | 0.5 day | | `--download` option | P2 | 0.5 day | --- ## 16. Out of Scope (v1) | Feature | Reason | |---------|--------| | Block explorer | Can add later, not critical for MVP | | L2 faucet | Anvil accounts have unlimited ETH | | Multiple L2 chains | Start with single chain | | Custom key configuration | Anvil keys are sufficient | | Windows native (non-Docker) | Docker is the supported path | --- ## 17. Security Considerations ### Local Development Only - Anvil's private keys are **publicly known** and hardcoded - Never use these keys on real networks - Bundle includes clear warnings in README ### No Sensitive Data - No real API keys in bundle - No real private keys (only Anvil's known keys) - No network access required --- ## 18. Appendix: Quick Reference ### Endpoints (After `docker compose up`) | Service | URL | |---------|-----| | L2 JSON-RPC | http://localhost:8545 | | L2 WebSocket | ws://localhost:8546 | | L1 (Anvil) | http://localhost:8547 | | op-node RPC | http://localhost:9545 | | op-alt-da | http://localhost:3100 | | popsigner-lite | http://localhost:8080 | ### Accounts (Pre-funded) | # | Address | ETH Balance | |---|---------|-------------| | 0 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 | 10000 | | 1 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 10000 | | 2 | 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC | 10000 | | ... | ... | ... | ### Commands ```bash docker compose up -d # Start docker compose down # Stop (preserves state) docker compose logs -f # View logs ./scripts/reset.sh # Reset to initial state ```