A tool to quickly bring up a devnet with:
- L1 node running
0xpartha/geth:latest - L2 node running
0xpartha/geth:latest - Native sequencer running
0xpartha/native-sequencer:latest
- ✅ Optional Genesis Generation: Generate genesis file with
--generate-genesisflag (10 wallets, 100 ETH each by default) - ✅ Pre-configured Accounts: All accounts are pre-funded and ready to use
- ✅ Safe Defaults: Uses existing genesis file by default (prevents accidental overwrites)
- ✅ Easy Configuration: Configurable via command-line arguments or environment variables
- ✅ Docker-based: Uses Docker containers for easy setup and cleanup
- ✅ Single Command: Start everything with one command (or start individual services)
- ✅ Individual Service Control: Start/stop individual services (L1, L2, sequencer) independently
- ✅ Automatic Block Production: L1 uses Clique PoA consensus (blocks every 5 seconds)
- ✅ Sequencer-driven L2: L2 blocks produced by sequencer via Engine API
- ✅ JWT Authentication: Automatically generates and configures JWT secret for Engine API authentication
- ✅ RPC Request Logging: Full RPC request/response logging enabled for debugging
- ✅ NativeRollup Contract: Automatically deploys NativeRollup contract on L1
- ✅ Foundry Auto-install: Automatically installs Foundry if not present
- Docker: Required to run geth and sequencer containers
- Install from: Docker Desktop
- Python 3: Required for genesis generation
- curl: Required for health checks (usually pre-installed)
-
eth-account Python library: For proper Ethereum address derivation
pip install eth-account
If not installed, the script uses a deterministic fallback method suitable for devnets.
-
Foundry: Automatically installed if not present (required for NativeRollup contract deployment)
- The script will attempt to install Foundry automatically
- Manual installation: Foundry Installation Guide
# First time: Generate genesis file and start devnet (all services)
./start-devnet.sh --generate-genesis
# Subsequent runs: Use existing genesis file and start all services (default behavior)
./start-devnet.sh
# Stop all services
./start-devnet.sh --stopImportant: The script uses existing genesis files by default. If no genesis file exists, you'll get an error prompting you to use --generate-genesis.
You can start individual services independently:
# Start only L1 node
./start-devnet.sh --start-l1
# Start only L2 node
./start-devnet.sh --start-l2
# Start only sequencer
./start-devnet.sh --start-sequencer
# Start all services (default behavior when no flags specified)
./start-devnet.shNote: When starting individual services, the script will:
- Only initialize and start the specified service
- Only clean up containers/ports for the specified service
- Deploy NativeRollup contract only if L1 is being started
- Create Docker network if needed
# Generate genesis with custom parameters and start
./start-devnet.sh --generate-genesis --num-wallets 20 --wallet-balance 200
# Start with custom ports (uses existing genesis)
./start-devnet.sh --l1-port 8545 --l2-port 18545 --sequencer-port 18547
# Clean start (removes existing data, then use existing genesis)
./start-devnet.sh --clean-data
# Clean start with new genesis generation
./start-devnet.sh --clean-data --generate-genesis
# Explicitly generate genesis file (overwrites existing)
./start-devnet.sh --generate-genesis
# Skip genesis generation (use existing genesis file - default behavior)
./start-devnet.sh --no-genesis
# Use custom Docker images
./start-devnet.sh --geth-image mygeth:latest --sequencer-image mysequencer:latest| Option | Description | Default |
|---|---|---|
--data-dir DIR |
Data directory | ./data |
--genesis-dir DIR |
Genesis directory | ./genesis |
--num-wallets N |
Number of wallets to create | 10 |
--wallet-balance ETH |
Balance per wallet in ETH | 100 |
--l1-chain-id ID |
L1 chain ID | 61971 |
--l2-chain-id ID |
L2 chain ID | 61972 |
--l1-port PORT |
L1 RPC port | 8545 |
--l2-port PORT |
L2 RPC port | 18545 |
--l2-engine-port PORT |
L2 Engine API port | 18551 |
--sequencer-port PORT |
Sequencer RPC port | 18547 |
--sequencer-metrics PORT |
Sequencer metrics port | 9090 |
--geth-image IMAGE |
Geth Docker image | 0xpartha/geth:latest |
--sequencer-image IMAGE |
Sequencer Docker image | 0xpartha/native-sequencer:latest |
--clean-data |
Clean data directories before starting | false |
--generate-genesis |
Generate genesis file (overwrites existing) | false (use existing) |
--no-genesis |
Skip genesis generation (default behavior) | true (default) |
--stop |
Stop all running containers | - |
--start-l1 |
Start only L1 node | - |
--start-l2 |
Start only L2 node | - |
--start-sequencer |
Start only sequencer | - |
--stop-l1 |
Stop only L1 node | - |
--stop-l2 |
Stop only L2 node | - |
--stop-sequencer |
Stop only sequencer | - |
--help |
Show help message | - |
Note: --no-genesis is the default behavior. The script will use existing genesis files and fail with a clear error message if the genesis file doesn't exist, prompting you to use --generate-genesis.
All command-line options can also be set via environment variables:
export DATA_DIR=./my-data
export NUM_WALLETS=20
export WALLET_BALANCE=200
export L1_PORT=8545
export L2_PORT=18545
./start-devnet.shAfter starting, the following services are available:
- L1 Node:
http://localhost:8545(default)- Uses Clique PoA consensus - automatically produces blocks every 5 seconds
- First account from genesis is the block signer/miner
- L2 Node:
http://localhost:18545(default)- Blocks are produced by the sequencer via Engine API
- No local mining - receives blocks from sequencer
- L2 Engine API:
http://localhost:18551(default)- Used by sequencer to submit blocks to L2 node
- Sequencer:
http://localhost:18547(default)- Produces L2 blocks and submits them via Engine API
- Sequencer Metrics:
http://localhost:9090(default)
The script automatically deploys the NativeRollup contract on L1 after all services are started:
- Contract address is displayed in the startup summary
- Uses the first account from genesis as the deployer
- Deterministic deployment (same address on each devnet start)
- Contract validates execute transaction chainId and calls EXECUTE precompile
When you generate a genesis file (with --generate-genesis), accounts are created and saved to:
genesis/accounts.txt- Contains addresses and private keys in format:address:private_keygenesis/genesis.json- Genesis file with pre-funded accounts
You can use the accounts in your applications:
# View accounts
cat genesis/accounts.txt
# Extract first account address
head -n 1 genesis/accounts.txt | cut -d: -f1
# Extract first account private key
head -n 1 genesis/accounts.txt | cut -d: -f2# Get first account
ACCOUNT=$(head -n 1 genesis/accounts.txt | cut -d: -f1)
PRIVATE_KEY=$(head -n 1 genesis/accounts.txt | cut -d: -f2)
# Send transaction via L1
curl -X POST http://localhost:8545 \
-H "Content-Type: application/json" \
-d "{
\"jsonrpc\": \"2.0\",
\"method\": \"eth_sendTransaction\",
\"params\": [{
\"from\": \"$ACCOUNT\",
\"to\": \"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb\",
\"value\": \"0x2386f26fc10000\"
}],
\"id\": 1
}"# L1 node logs (includes RPC request logging)
docker logs -f native-quickstart-l1
# L2 node logs (includes RPC request logging)
docker logs -f native-quickstart-l2
# Sequencer logs
docker logs -f native-quickstart-sequencerNote: Both L1 and L2 nodes have RPC request logging enabled (--vmodule "rpc=5,http=5"). You'll see detailed logs of all JSON-RPC requests and responses, making it easy to debug and monitor interactions with the nodes.
# Check L1 block number (should increase every 5 seconds)
cast block-number --rpc-url http://localhost:8545
# Check L2 block number (produced by sequencer)
cast block-number --rpc-url http://localhost:18545
# Watch L1 blocks being produced
watch -n 1 'cast block-number --rpc-url http://localhost:8545'# Check if containers are running
docker ps | grep native-quickstart
# Check L1 node
curl -X POST http://localhost:8545 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Check L2 node
curl -X POST http://localhost:18545 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Check sequencer metrics
curl http://localhost:9090# Stop all containers
./start-devnet.sh --stop
# Stop individual services
./start-devnet.sh --stop-l1 # Stop only L1
./start-devnet.sh --stop-l2 # Stop only L2
./start-devnet.sh --stop-sequencer # Stop only sequencer
# Or manually stop containers
docker stop native-quickstart-l1 native-quickstart-l2 native-quickstart-sequencer
docker rm native-quickstart-l1 native-quickstart-l2 native-quickstart-sequencerThe script automatically generates and configures JWT authentication for Engine API communication between the sequencer and L2 geth:
- JWT Secret Generation: A 32-byte JWT secret is automatically generated on first run and stored at
data/jwt.hex - L2 Geth Configuration: The JWT secret is mounted into the L2 geth container and configured via
--authrpc.jwtsecret - Sequencer Configuration: The same JWT secret is passed to the sequencer via the
L2_JWT_SECRETenvironment variable - Persistence: The JWT secret persists across restarts (unless
--clean-datais used)
Note: The JWT secret file (data/jwt.hex) is automatically added to .gitignore for security.
Data is stored in the data/ directory by default:
data/l1/- L1 node datadata/l2/- L2 node datadata/sequencer/- Sequencer datadata/jwt.hex- JWT secret for Engine API authentication (auto-generated)
To start fresh:
./start-devnet.sh --clean-dataNote: Using --clean-data will also remove the JWT secret file, generating a new one on the next run.
The genesis file is generated in genesis/genesis.json and includes:
- Pre-funded accounts (10 wallets with 100 ETH each by default)
- Chain configuration
- Network ID
- Clique PoA consensus configuration (for L1 block production)
- Block period: 5 seconds
- First account is configured as the signer
The script defaults to using existing genesis files for safety:
- Default (
--no-genesis): Use existing genesis file- If genesis file exists: Uses it and starts the devnet
- If genesis file doesn't exist: Shows error message prompting to use
--generate-genesis
--generate-genesis: Generate a new genesis file (overwrites existing if present)
Error Message: If you run the script without --generate-genesis and no genesis file exists, you'll see:
❌ Error: Genesis file not found: ./genesis/genesis.json
Please run the script with --generate-genesis option to create a new genesis file:
./start-devnet.sh --generate-genesis
Or create genesis.json manually in ./genesis
Examples:
# First time setup: Generate genesis and start
./start-devnet.sh --generate-genesis
# Subsequent runs: Use existing genesis (default)
./start-devnet.sh
# Regenerate genesis with custom parameters
./start-devnet.sh --generate-genesis --num-wallets 20 --wallet-balance 200
# Generate genesis file manually, then start
./generate-genesis.sh --num-wallets 10 --wallet-balance 100
./start-devnet.shIf a port is already in use, specify different ports:
./start-devnet.sh --l1-port 28545 --l2-port 28546 --sequencer-port 28547Check logs:
docker logs native-quickstart-l1
docker logs native-quickstart-l2
docker logs native-quickstart-sequencerError: "Genesis file not found"
❌ Error: Genesis file not found: ./genesis/genesis.json
Please run the script with --generate-genesis option to create a new genesis file:
./start-devnet.sh --generate-genesis
Solution: Run with --generate-genesis flag:
./start-devnet.sh --generate-genesisRegenerate existing genesis file:
./start-devnet.sh --generate-genesisClean start with new genesis:
./start-devnet.sh --clean-data --generate-genesisThe script will try to use local images if pull fails. Make sure you have the images:
docker pull 0xpartha/geth:latest
docker pull 0xpartha/native-sequencer:latestIf you encounter issues with account generation, install the eth-account library:
pip install eth-account-
L1 Node: Uses Clique (Proof of Authority) consensus
- Automatically produces blocks every 5 seconds
- First account from genesis is the signer/miner
- No beacon node required
- Block rewards go to the signer account
-
L2 Node: Receives blocks from sequencer
- No local mining/mining flags
- Blocks are produced by the sequencer
- Sequencer submits blocks via Engine API (
engine_newPayloadV2,engine_forkchoiceUpdatedV2) - Standard rollup architecture
-
L1: Clique PoA (Proof of Authority)
- Simple consensus for devnets
- Automatic block production
- No external dependencies
-
L2: Engine API (PoS-like)
- Sequencer-driven block production
- Compatible with standard rollup architecture
- Engine API for block submission
You can start services one at a time for debugging or development:
# Start L1 first
./start-devnet.sh --start-l1
# Wait for L1 to be ready, then start L2
./start-devnet.sh --start-l2
# Finally start the sequencer
./start-devnet.sh --start-sequencerUse Cases:
- Debug individual service startup issues
- Test services independently
- Start services in a specific order
- Restart a single service without affecting others
Note: When starting individual services:
- The Docker network is created automatically if needed
- Only the specified service's containers are cleaned up
- NativeRollup contract is deployed only when starting L1 (or all services)
# Use custom chain IDs (defaults are 61971 for L1, 61972 for L2)
./start-devnet.sh --l1-chain-id 1337 --l2-chain-id 1338Note: If you change chain IDs, make sure to regenerate the genesis file:
./start-devnet.sh --clean-data --generate-genesis --l1-chain-id 1337 --l2-chain-id 1338Run multiple devnets on different ports:
# Devnet 1
DATA_DIR=./data1 ./start-devnet.sh --l1-port 8545 --l2-port 18545 --sequencer-port 18547
# Devnet 2
DATA_DIR=./data2 ./start-devnet.sh --l1-port 28545 --l2-port 28546 --sequencer-port 28547The NativeRollup contract is automatically deployed on L1 after all services start. The contract address is displayed in the startup summary.
Contract Features:
- Validates chainId from execute transaction calldata
- Calls EXECUTE precompile (address
0x12) to validate witness data - Returns gas consumed or errors on failure
- Located in
native-rollup/directory
Using the Contract:
# Contract address is shown in startup summary
# Example usage with cast:
cast call <CONTRACT_ADDRESS> "chainId()" --rpc-url http://localhost:8545The devnet can be integrated with other development tools:
# Set environment variables for your tools
export L1_RPC_URL=http://localhost:8545
export L2_RPC_URL=http://localhost:18545
export SEQUENCER_RPC_URL=http://localhost:18547
# Your tool can now connect to the devnetBoth L1 and L2 nodes log all RPC requests with detailed information:
- Incoming JSON-RPC requests
- Method calls and parameters
- HTTP request details
- Response data
View logs to see all RPC activity:
# View L1 RPC requests
docker logs -f native-quickstart-l1 | grep -i rpc
# View L2 RPC requests
docker logs -f native-quickstart-l2 | grep -i rpcSee LICENSE file.