Local Devnet
The base/base repository ships a Docker devnet that brings up the whole Base stack locally: an L1 made of Reth and Lighthouse, a Base sequencer, two RPC nodes, and a batcher. A shadow batcher and shadow validator run alongside it so the Rust batcher gets exercised too. It is the place to try contracts, apps, and node or hardfork changes when you do not want a public network involved.
There is also a snapshot dev chain that picks up from real Base Mainnet or Sepolia state instead of a fresh genesis. The table further down helps you choose.
If you only need to build against new Base features and have no interest in running infrastructure, the hosted Vibenet is the lighter option.
Prerequisites
Section titled “Prerequisites”You need the following before the first start:
- Docker with Compose v2 and Buildx. The first build compiles the Rust services, so give Docker plenty of memory.
just1.51.0or newer.- Foundry, for
cast. jq, whichjust devnet statusandjust devnet checksrely on.- A clone of
base/basewith the other prerequisites in place.
Pre-Pulling Images
Section titled “Pre-Pulling Images”To shorten the first start, build the setup image and pull the L1 images ahead of time:
just devnet pull-imagesStarting the Devnet
Section titled “Starting the Devnet”The devnet commands live in a just module, so each one is written just devnet <command>. Running just devnet on its own only prints the list.
Run this from the root of the repo:
just devnet up-singleIt shuts down any devnet that is already running, clears the old data, builds fresh images, and starts a stack with one sequencer. Expect the first run to take several minutes while the Rust services compile.
| Command | Starts |
|---|---|
just devnet up-single | One sequencer. The quickest choice for most testing. |
just devnet up | A 3-node high-availability sequencer cluster with conductor. |
Confirm that blocks are being produced:
just devnet statusStopping the Devnet
Section titled “Stopping the Devnet”just devnet downThis removes the containers and deletes everything under .devnet/, so the next start is clean.
Sending a Test Transaction
Section titled “Sending a Test Transaction”The standard Foundry/Anvil test accounts are funded at genesis. Their addresses and keys are in etc/docker/devnet-env. This sends a little ETH through the sequencer:
source etc/docker/devnet-envcast send "$ANVIL_ACCOUNT_2_ADDR" --value 0.001ether \ --private-key "$ANVIL_ACCOUNT_1_KEY" \ --rpc-url http://127.0.0.1:7545A receipt showing status 1 means the sequencer took the transaction into a block. For a canned set of L1 and L2 checks, run just devnet checks instead.
Endpoints
Section titled “Endpoints”| Service | URL | Chain ID |
|---|---|---|
| L2 sequencer (builder) RPC | http://127.0.0.1:7545 | 84538453 |
| L2 RPC node | http://127.0.0.1:8545 | 84538453 |
L2 RPC node (base-rpc) | http://127.0.0.1:8645 | 84538453 |
| L1 RPC | http://127.0.0.1:4545 | 1337 |
| Grafana | http://localhost:3000/d/devnet-overview |
All ports come from etc/docker/devnet-env. With just devnet up, port 7545 is only one of three sequencers that conductor manages. If leadership moves, a transaction sent there can sit pending. Send through the RPC node on 8545 instead, because it forwards to whichever sequencer currently leads.
Hardfork Activation
Section titled “Hardfork Activation”A fresh devnet crosses every scheduled upgrade almost at once. Azul activates at L2 block 20, Beryl at 21, Cobalt at 22, and Denim at 25. Blocks come every 2 seconds until Denim, and then the sequencer drops to 200ms blocks.
Two optional modes add the unscheduled forks:
| Command | Also activates |
|---|---|
just devnet up-single everest | Everest (Base Transactions, type 0x79) at block 100 |
just devnet up-single zenith | Everest and Zenith at block 100 |
Each activation reads from an L2_BASE_<HARDFORK>_BLOCK variable, such as L2_BASE_DENIM_BLOCK. Set it before up or up-single to move that fork. An empty value leaves the fork unscheduled.
Common Commands
Section titled “Common Commands”| Command | Does |
|---|---|
just devnet status | Prints block numbers and sync status |
just devnet accounts | Lists funded test accounts with balances and nonces |
just devnet smoke | Sends test transactions to L1 and L2 |
just devnet checks | Runs status and then smoke |
just devnet logs [containers] | Streams logs, for all containers or the ones you name |
just devnet ps | Shows container status |
just devnet flashblocks | Streams Flashblocks from the builder until Denim (block 25 by default). Nothing arrives after Denim. Needs Go; the first run installs the client and can exit silently, so run it a second time. |
just devnet profiling | Restarts the devnet with Pyroscope and optimized profiling builds |
just devnet tests | Runs the system tests against the devnet |
just devnet down | Stops the devnet and deletes its data |
The devnet README in etc/docker/README.md and the recipes in etc/docker/Justfile are the full reference. They also cover the Anvil + Nitro proving variant and upgrade-signal testing.
Load Testing
Section titled “Load Testing”The load generator is a separate module that targets the devnet by default:
just load-test-continuousProving
Section titled “Proving”No prover runs inside the devnet. To prove against it, start the separate standalone proving stack. ZK proving against a local devnet only works with Beryl and Cobalt turned off.
Snapshot Dev Chain (base dev)
Section titled “Snapshot Dev Chain (base dev)”There is a second, lighter way to get a local chain. base dev takes a copy of a real snapshot and keeps producing blocks on top of it, with no L1 behind it. One process holds both an embedded execution node and a standalone sequencer, talking over Engine IPC. Contracts, balances, and storage start out exactly as they were on the network at the snapshot head.
Picking Between the Two
Section titled “Picking Between the Two”| Docker devnet | Snapshot dev chain | |
|---|---|---|
| Starting state | A fresh genesis | Base Mainnet or Sepolia at the snapshot head |
| Good for | Sequencing, derivation, batching, fork activation, HA failover | Real deployed contracts and liquidity, acting as any account, 200ms blocks |
| Out of reach | Mainnet contracts and state | L1 derivation, bridges, safe and finalized heads, proofs |
| Needs | Docker | A native Rust build and at least 1.5 TB of free disk; no Docker |
| First start | Several minutes of image builds | About an hour of download, then a one-time index rebuild |
Pick the Docker devnet when the test touches node behavior or L1. Pick base dev when you want your app to meet the same contracts it will see on Mainnet.
Requirements
Section titled “Requirements”On top of just, Foundry, and jq, the recipes compile base with Cargo, so you need:
- The Rust toolchain pinned in
rust-toolchain.toml(through rustup), Go, a C/C++ compiler, libclang, OpenSSL development headers,pkg-config, CMake, andprotoc. - At least 1.5 TB free, ideally on fast NVMe. A Base Mainnet download in October 2026 came to about 830 GiB and unpacked to about 1.4 TB. Upstream has only tested the flow on Linux.
Download and Run
Section titled “Download and Run”just devnet snapshot download --dir "$HOME/data/my-devnet"just devnet snapshot run --dir "$HOME/data/my-devnet"download only fetches data and writes a node config into the directory. It does not start anything. Add --print-plan-json to list the exact archives and sizes first. If the transfer breaks off, run the same command again and verified archives are reused. Add --manifest-url <url> to stay on one specific snapshot across a resume.
The download keeps 1,339,200 blocks of transactions, receipts, and state history, which covers about 31 days of 2-second blocks. The --full preset keeps 13,392,000 blocks instead. That is 31 days only once blocks are 200ms, and about 310 days today, so a Mainnet --full pull is about 1.9 TB and unpacks to about 3.7 TB.
run builds base, prints the endpoints and the snapshot head, and stays in the foreground. The snapshot ships without the transaction and history indexes, so the first run rebuilds them. RPC calls work within seconds, but no new blocks appear until the rebuild ends. Upstream measured about 22 minutes for a Mainnet snapshot. Later starts begin sequencing almost at once.
What else to expect:
- It never creates, downloads, or resets data on its own.
runonly continues a datadir that already exists. - Peers and discovery are off. HTTP RPC listens on
http://127.0.0.1:8545and WS onws://127.0.0.1:8546, with theeth_*,debug_*, andtrace_*methods.--http.portand--ws.portmove them. - The chain ID stays the snapshot’s own:
8453for Mainnet,84532for Sepolia. - If the snapshot head is from before Denim, Denim activates on the next block. The schedule and starting head are written to
base-dev.jsonin the datadir and reused after a restart. - Ctrl+C stops it, and the same command resumes it. Downtime does not create catch-up blocks.
For Sepolia, select the chain on both steps. Note that download refuses the BASE_CHAIN variable and takes its own --chain flag:
env -u BASE_CHAIN just devnet snapshot download --chain sepolia --dir "$HOME/data/sepolia-devnet"BASE_CHAIN=sepolia just devnet snapshot run --dir "$HOME/data/sepolia-devnet"Block Timing
Section titled “Block Timing”Blocks come every 200ms, but header timestamps only count whole seconds, so roughly five blocks in a row share one timestamp. Code that counts blocks sees time run ten times faster than on a 2-second chain. Code that reads block.timestamp sees normal time. A busy disk or CPU can make blocks late.
dev_impersonateTransaction
Section titled “dev_impersonateTransaction”base dev adds one RPC method that runs a call as any account without its key. It is handy for funding accounts, moving tokens out of real holders, or calling owner-only functions:
dev_impersonateTransaction({from, to?, input?, value?, gas, mint?}) -> txHashfromandgasare required.inputdefaults to empty, andvalueandmintdefault to zero.mintwei is credited tofrombefore the call runs. Leave outtoto deploy a contract frominput.- The request becomes a deposit transaction. Signature, nonce, and fee checks are skipped, and no fee is paid. Checks that live inside contracts, such as
ecrecover, EIP-1271, orpermit, still run. Use signed transactions when you want to test admission or fees. - The receipt has type
0x7e, aneffectiveGasPriceof zero, andfromset to the impersonated account. Each request bumps that account’s nonce. - The returned hash only means the request is queued. It normally lands in the next block, about 200ms later. Poll
eth_getTransactionReceiptrather than sending again, because a repeat is a second transaction. A reverting call still lands, with status0, and any minted ETH stays. Queued requests live in memory and vanish if the process stops.
A quick way to fund a fresh key with 1 ETH:
RPC=http://127.0.0.1:8545DEV_KEY=$(cast wallet new --json | jq -er '(if type == "array" then . else .data end)[0].private_key')ACCOUNT=$(cast wallet address --private-key "$DEV_KEY")cast rpc --rpc-url "$RPC" dev_impersonateTransaction \ "{\"from\":\"$ACCOUNT\",\"to\":\"$ACCOUNT\",\"gas\":\"0x5208\",\"mint\":\"0xde0b6b3a7640000\"}"| Error code | Meaning |
|---|---|
-32602 | Bad request: from or gas missing, gas under the intrinsic cost or above the block gas limit minus 2,000,000, calldata over 131,072 bytes, or init code over 49,152 bytes |
-32000 | Try again later: sequencing has not started (for example during the first-run rebuild), or 256 requests are already waiting |
Troubleshooting
Section titled “Troubleshooting”Containers fail to start. Check that Docker is running and that your user belongs to the docker group. docker compose version confirms the Compose plugin is present.
Port conflicts. Stop whatever else holds the port, or change the value in etc/docker/devnet-env.
Stale state. If behavior looks wrong after a code change, run just devnet up-single again. It always clears data before it starts.
Slow first start. The first run pulls images and compiles the Rust services. Running just devnet pull-images beforehand saves part of that wait.
Transactions pending in HA mode. After a leader change, send through 8545 rather than 7545.