Skip to content
BaseHub by Will Binns Updated

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.

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.
  • just 1.51.0 or newer.
  • Foundry, for cast.
  • jq, which just devnet status and just devnet checks rely on.
  • A clone of base/base with the other prerequisites in place.

To shorten the first start, build the setup image and pull the L1 images ahead of time:

Terminal window
just devnet pull-images

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:

Terminal window
just devnet up-single

It 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.

CommandStarts
just devnet up-singleOne sequencer. The quickest choice for most testing.
just devnet upA 3-node high-availability sequencer cluster with conductor.

Confirm that blocks are being produced:

Terminal window
just devnet status
Terminal window
just devnet down

This removes the containers and deletes everything under .devnet/, so the next start is clean.

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:

Terminal window
source etc/docker/devnet-env
cast send "$ANVIL_ACCOUNT_2_ADDR" --value 0.001ether \
--private-key "$ANVIL_ACCOUNT_1_KEY" \
--rpc-url http://127.0.0.1:7545

A 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.

ServiceURLChain ID
L2 sequencer (builder) RPChttp://127.0.0.1:754584538453
L2 RPC nodehttp://127.0.0.1:854584538453
L2 RPC node (base-rpc)http://127.0.0.1:864584538453
L1 RPChttp://127.0.0.1:45451337
Grafanahttp://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.

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:

CommandAlso activates
just devnet up-single everestEverest (Base Transactions, type 0x79) at block 100
just devnet up-single zenithEverest 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.

CommandDoes
just devnet statusPrints block numbers and sync status
just devnet accountsLists funded test accounts with balances and nonces
just devnet smokeSends test transactions to L1 and L2
just devnet checksRuns status and then smoke
just devnet logs [containers]Streams logs, for all containers or the ones you name
just devnet psShows container status
just devnet flashblocksStreams 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 profilingRestarts the devnet with Pyroscope and optimized profiling builds
just devnet testsRuns the system tests against the devnet
just devnet downStops 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.

The load generator is a separate module that targets the devnet by default:

Terminal window
just load-test-continuous

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.

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.

Docker devnetSnapshot dev chain
Starting stateA fresh genesisBase Mainnet or Sepolia at the snapshot head
Good forSequencing, derivation, batching, fork activation, HA failoverReal deployed contracts and liquidity, acting as any account, 200ms blocks
Out of reachMainnet contracts and stateL1 derivation, bridges, safe and finalized heads, proofs
NeedsDockerA native Rust build and at least 1.5 TB of free disk; no Docker
First startSeveral minutes of image buildsAbout 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.

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, and protoc.
  • 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.
Terminal window
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. run only continues a datadir that already exists.
  • Peers and discovery are off. HTTP RPC listens on http://127.0.0.1:8545 and WS on ws://127.0.0.1:8546, with the eth_*, debug_*, and trace_* methods. --http.port and --ws.port move them.
  • The chain ID stays the snapshot’s own: 8453 for Mainnet, 84532 for 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.json in 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:

Terminal window
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"

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.

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?}) -> txHash
  • from and gas are required. input defaults to empty, and value and mint default to zero. mint wei is credited to from before the call runs. Leave out to to deploy a contract from input.
  • 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, or permit, still run. Use signed transactions when you want to test admission or fees.
  • The receipt has type 0x7e, an effectiveGasPrice of zero, and from set 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_getTransactionReceipt rather than sending again, because a repeat is a second transaction. A reverting call still lands, with status 0, 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:

Terminal window
RPC=http://127.0.0.1:8545
DEV_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 codeMeaning
-32602Bad 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
-32000Try again later: sequencing has not started (for example during the first-run rebuild), or 256 requests are already waiting

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.