Tutorial: Operating a Private Network¶
This tutorial builds a private network on one machine: a chain with its own genesis block that only your nodes run. The numbered steps use proof of work, with a CPU miner. Proof of authority with Clique replaces the miner with signers. Every command on this page was run before it was published, and the output shown is from that run. To build a network into the client instead, with a flag of its own, see Add a network.
The steps: 1. Genesis · 2. Initialize · 3. Bootnode · 4. Member node · 5. Miner · 6. Confirm · 7. Stop
Signers instead of a miner: Proof of authority with Clique
Before you start¶
You need geth and bootnode from the same release. bootnode ships in the alltools archive (What else is in a release), and a build from source produces it too.
A private network runs with no network flag. With no network flag, geth runs Ethereum Classic. A data directory already initialized with another genesis keeps running that chain instead (Supported networks). Three things follow, and every command below is written for them:
- Every command for a node names its
--datadir.geth initwithout--datadirwrites your genesis into the default Ethereum Classic data directory. A node started on a directory nobody initialized writes Ethereum Classic’s genesis there, and becomes an Ethereum Classic node, chain ID 61. - Ethereum Classic’s network defaults still apply, so each node overrides them.
--bootnodesreplaces its bootnodes with yours,--discovery.dns ""switches off its DNS discovery lists, and--nat nonestopsgethlooking up its public address and asking your router to forward its ports. Agethnode that is itself the bootnode takes--bootnodes "", so it contacts no Ethereum Classic bootnode. --networkidis the chain ID fromgenesis.json. Without it,gethtakes the network ID from the chain ID anyway, but its first log line readsStarting Core-Geth on Ethereum Classic....
Every node on one machine needs its own ports. A node started on a port another node holds exits with bind: address already in use. The ports on this page are arbitrary, and stay clear of the ports a node takes by default (Ports and listeners).
1. Define the genesis state¶
Every node on the network starts from the same genesis block, defined in a JSON file. Save this as genesis.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
chainIdis your chain’s ID, and every node passes it as--networkid. Choose a value no public network uses.configlists the protocol upgrades the chain runs from its first block. This example stops at Petersburg; the JSON genesis on Add a network activates, from block 0, every EIP thatClassicChainConfigsets."ethash": {}selects proof of work, which step 2 confirms.noncemakes the genesis block yours. Nodes whose genesis block differs from yours cannot peer with your nodes (at--verbosity 4, both sides loggenesis mismatch), and nodes with an identical one can, so change it to a value of your own.allocfunds accounts at genesis, with balances in wei:
1 2 3 | |
2. Initialize every data directory¶
Write the genesis into each node’s data directory before that node first starts. This tutorial runs two nodes, node1 and node2:
1 2 | |
Each init reports the genesis it wrote, then repeats the lines for lightchaindata:
1 2 3 | |
Check for Chain ID: 12345 and Consensus: Ethash (proof-of-work). Without the ethash object, the same file reports Consensus: unknown.
3. Start a bootnode¶
Bootnodes and peer discovery covers bootnodes on the public networks.
The bootnode is how the nodes find each other. Generate its key once, then start it, and leave it running:
1 2 | |
It prints its enode URL:
1 2 3 | |
- The URL names
127.0.0.1, which reaches nodes on the same machine. For nodes on other machines, replace it with an address they can reach this one on. - The bootnode listens on UDP only. Allow UDP on its port through any firewall between the machines. A TCP check such as
telnetcannot connect to it, whether it is running or not. - The same
boot.keygives the same URL every time the bootnode starts.
4. Start a member node¶
In a new terminal, start node1, pointed at the bootnode:
1 | |
Quote the enode URL. It contains ?, and zsh, by default, refuses to run a command with an unquoted ? that matches no file.
The log shows the private genesis loading:
1 2 | |
5. Start a miner¶
Mining covers the mining flags in more detail.
The miner is a node like node1, with mining switched on. Its rewards go to an account whose key you hold, so create one in node2’s data directory:
1 | |
1 2 3 4 | |
Then, in another terminal, start node2:
1 2 | |
--miner.etherbaseis required. Set to the zero address,gethexits withFatal: Failed to start mining: etherbase missing: etherbase must be explicitly specified.--miner.threads=1is what mines.--minealone logsUpdated mining threads threads=0, runs no CPU search and seals no block: a node started that way was still at block 0 after 90 seconds. One thread is enough for a private network.--miner.gaslimitis the gas ceiling blocks move toward; Configuration gives the ceilinggethuses when you do not set it.--miner.gaspriceis the lowest gas price the miner accepts; locally submitted transactions are exempt.
Before its first block, the miner generates the Ethash dataset (the DAG) for the current epoch, and it generates the next epoch’s while it mines. Each is about 1 GB, is written to ~/.etchash on Linux rather than to the data directory (--ethash.dagdir moves it), and occupies every CPU core while it is generated:
1 2 3 4 5 6 | |
This is Ethash, not Etchash. Ethereum Classic and Mordor mine Etchash, the variant ECIP-1099 introduced, whose epochs are 60,000 blocks (epochLengthECIP1099 in consensus/ethash/algorithm.go). A chain from this genesis mines the original Ethash, with the 30,000-block epochs its log shows (epochLengthDefault in the same file).
6. Confirm the network works¶
Attach to node1, which does not mine, and check three things: it has a peer, its chain grows, and the miner’s account is being paid:
1 2 3 | |
1 2 3 | |
The peer is node2, the blocks are the ones it mined, and the balance is in wei. Your numbers will differ, and grow as the miner runs.
7. Stop the network¶
Press Ctrl-C in each node’s terminal, and in the bootnode’s. A node logs Got interrupt, shutting down... and exits after Blockchain stopped:
1 2 3 | |
To restart, start the bootnode and both nodes with the same commands, skipping bootnode --genkey and geth account new. The data directories keep the chain: node1 restarted at block 8, where it stopped.
Proof of authority with Clique¶
Clique replaces the miner with signers. The accounts listed in the genesis block take turns sealing a block every period seconds, and a majority of them can add or remove a signer (EIP-225). This section builds a two-signer network with the same bootnode and network flags as the steps above. Work in a new directory: it is a separate chain.
Create the signer accounts¶
1 2 | |
Each prints its address, as in step 5. Save each password in a file readable only by you, in that data directory: signer1/password.txt and signer2/password.txt. A signer reads it when it starts, to unlock its account.
Define a Clique genesis¶
Save this as genesis.json, with the two signer addresses in extraData:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
extraDatalists the initial signers: 32 zero bytes (64 hex zeros), then each signer’s address without0x, in ascending order, then 65 zero bytes (130 hex zeros).clique, and noethash. With both objects present, this file runs Clique, whileinitand the node log reportConsensus: Ethash (proof-of-work), so the check in step 2 would mislead you.periodis the number of seconds between blocks.epochis the number of blocks between checkpoints, which reset pending votes.
Initialize both data directories:
1 2 | |
Check for Consensus: Clique (proof-of-authority):
1 | |
Start the signers¶
Start a bootnode as in step 3. Then, in a new terminal, start signer1:
1 2 | |
In another terminal, start signer2:
1 2 | |
--unlockand--passwordare required. A signer started without them logsBlock sealing failed err="authentication needed: password or unlock"and seals no block.- Keep a signer’s RPC private.
gethrefuses to unlock an account while HTTP or WebSocket RPC is enabled, unless--allow-insecure-unlockis set, and exits withFatal: Account unlock with HTTP access is forbidden!
Each signer logs its account unlocking, then the blocks it seals:
1 2 3 | |
Confirm both signers seal¶
1 | |
1 2 3 4 5 6 7 8 | |
Both signers are sealing when both counts in sealerActivity are above zero. The counts cover the most recent blocks, up to 64. clique.getSigners() lists the signers the chain currently accepts, in address order.
Change the signer set¶
Each signer votes from its own node with clique.propose: true to add an address, false to remove one. It casts the vote in the blocks it seals. With two signers, one vote is not a majority, so a few blocks after signer1 votes, the signer set is unchanged:
1 2 | |
1 2 | |
A few blocks after signer2 votes too, the new signer is in:
1 2 | |
1 2 | |
clique.proposals shows the proposals a node votes for. Once a change has taken effect, clear its proposal on each signer with clique.discard("<new signer address>"). Adding an address starts no node for it: the two running signers kept the chain going, and clique.status() counted blocks for both of them and none for the new signer.
Stop the signers and the bootnode as in step 7.
This tutorial was run before it was published¶
Every command above was run on 13 September 2026, on one Linux x86_64 machine with an Intel Core i7-10710U (12 threads) and 31 GiB of RAM, with geth and bootnode built from source. The output is quoted from that run, with account addresses and enode IDs replaced by placeholders.