← Course home Module 17 / 19 Blockchain — Part 3

Hyperledger Besu: Hands-on Private Networks

Besu is the enterprise Ethereum client. Same Solidity, same Hardhat, same viem — but private networks, permissioning, and privacy groups. This is the only hands-on module in Part 3.

~75 min read Module 17 🛠️ Part 3
📌 Section 1

Recap: From Landscape to Keyboard

In Module 16 you surveyed the enterprise blockchain landscape — Besu, Fabric, Corda, Indy, Quorum — and learned when you would reach for each one. This module is different: it is the only hands-on module in Part 3. By the end, you will be running a real 4-node Besu network on your laptop and deploying a Solidity contract to it.

🎯

Why Besu First?

Besu is the most accessible enterprise blockchain for a developer coming out of Part 2. You already know Solidity. You already know Hardhat. You already know viem, MetaMask, OpenZeppelin, and Ignition. On Besu, all of that still works, unchanged. The only thing that changes is the network setup — how you start the chain, who is allowed to validate, and how you point your config at it.

🔁

What Changes vs. Part 2

In Part 2, you developed against a local Hardhat Network or Anvil, then deployed to Sepolia. In this module, you replace both of those with a consortium chain you control: a private Besu network with its own genesis, its own validators, its own RPC endpoint. Your contracts, tests, and scripts do not need to change at all.

🏁

Module Goal

By the end of this module you should be able to generate a Besu genesis file, launch a 4-node QBFT network in Docker, deploy a Solidity contract to it with Hardhat Ignition, talk to it from viem, add a permissioning rule, and explain how private transactions with Tessera work. That is a full enterprise Ethereum workflow end-to-end.

💡

Why Fabric Stays Conceptual

Module 18 (Fabric) will not be hands-on: Fabric uses Go/JavaScript chaincode, X.509 MSPs, channel configtxs, and a completely different mental model. Setting it up is a course of its own. Since Besu reuses everything you already know, it is the perfect place to invest real keyboard time.

⚙️ Section 2

What is Besu?

Hyperledger Besu is an open-source Ethereum client written in Java, built by ConsenSys and donated to the Linux Foundation's Hyperledger project in 2019. It is the only Ethereum client hosted at Hyperledger, and the only one designed from day one for both public mainnet and private enterprise networks.

Java-based Ethereum Client

Besu runs on the JVM, which makes it a natural fit for enterprise environments where Java is already the dominant language. It implements the full Ethereum specification: EVM, JSON-RPC, P2P devp2p networking, account model, Merkle-Patricia tries — everything. Running Besu on public Ethereum is exactly the same experience as running Geth, Erigon, or Nethermind.

🌐

Public OR Private

Most Ethereum clients can only sync to public mainnet. Besu can do that, and it can boot a brand new private chain with its own genesis, its own validator set, and its own consensus algorithm. One binary, two very different use cases.

🏢

Enterprise Features Built-In

On top of the Ethereum protocol, Besu adds three features you will not find on Geth: permissioning (who can connect and who can transact), privacy groups (private transactions via Tessera), and BFT consensus (IBFT 2.0 and QBFT) for known validator sets.

🛠️

Full Developer Tooling

Because Besu speaks the same JSON-RPC as Geth, Hardhat, Foundry, Remix, MetaMask, viem, Ethers, Truffle, Ignition, Tenderly, and every other Ethereum tool work against Besu with zero changes. Your entire Part 2 toolchain is already compatible.

📜

Apache 2.0 Licensed

Besu is fully open-source under the permissive Apache 2.0 license. You can run it, fork it, embed it in commercial products, and deploy it in production without paying anyone. That is a big deal for enterprises with strict licensing review processes.

🚀

Used in Real Deployments

Besu powers several central bank digital currency pilots, the Palm Network (NFTs for creators), the migration path away from JPMorgan's GoQuorum, and many internal banking consortia. It is not a toy — it is production software.

The Promise of Besu

If you can write Solidity, you can ship on Besu. Everything you learned in Part 2 is still valid. The only new skill is operating the network — and this module teaches exactly that.

🛡️ Section 3

Consensus: IBFT 2.0 and QBFT

Public Ethereum uses Proof of Stake. Bitcoin uses Proof of Work. Neither makes sense for a private consortium where you already know and trust (or contractually constrain) the validators. Besu instead offers two Byzantine Fault Tolerant consensus algorithms designed for this exact setting.

⚖️

Byzantine Fault Tolerant Consensus

A BFT algorithm guarantees that a network of N known validators can reach agreement on the next block as long as strictly fewer than (N − 1) / 3 validators are Byzantine (malicious, crashed, or silently wrong). For a 4-node network, that is at most 1 bad validator. For a 7-node network, up to 2. This is the classic one-third fault tolerance result from distributed-systems theory.

Instant Finality, No Reorgs

Unlike Proof of Work (probabilistic finality — “wait 6 confirmations”) or even Proof of Stake (“wait for two epochs”), BFT consensus offers instant, deterministic finality. Once a block is committed, it is final forever. There are no reorgs, no uncle blocks, no waiting. This is exactly what enterprise settlement systems need.

🏛️

IBFT 2.0 (Istanbul BFT)

IBFT 2.0 is the original BFT protocol in Besu, derived from Istanbul BFT and the PBFT family. It uses a round-robin proposer rotation, a three-phase commit (pre-prepare → prepare → commit), and on-chain validator voting to add or remove members. It has been battle-tested in production for years.

🛡️

QBFT (Quorum BFT)

QBFT is the newer, recommended BFT algorithm in Besu, originally developed for JPMorgan's Quorum and now the default for new deployments. It improves on IBFT 2.0 with better liveness under network partitions, clearer formal specification, more robust view changes, and smoother validator set management. If you are starting a new network today, pick QBFT.

IBFT 2.0 vs QBFT — Quick Comparison

Both algorithms share the same fundamental guarantees (instant finality, (N−1)/3 fault tolerance, on-chain validator voting). The differences are mostly in robustness, specification, and recommendation status.

Property
IBFT 2.0
QBFT
Finality
Instant
Instant
Fault tolerance
(N−1) / 3
(N−1) / 3
Formal spec
Informal / legacy
Formally specified
Liveness on partitions
Weaker
Improved
View changes
Basic
More robust
Recommended for new networks
No (legacy)
Yes (default)
📌

Practical Rule

For new Besu networks in 2025+, use QBFT. IBFT 2.0 remains supported for backwards compatibility with existing deployments. Both are configured through the genesis.json file you will generate in the next section.

🧱 Section 4

Network Setup: A 4-Node QBFT Cluster

Enough theory. Let's build a real 4-node QBFT network running locally in Docker Compose. This is the exact workflow you would use for a development or staging environment, scaled down to fit on your laptop.

1. Generate the Blockchain Config

Besu ships with a powerful subcommand, besu operator generate-blockchain-config, which takes a small JSON input and produces a full genesis file plus one key pair per validator. You specify the number of validators, the consensus algorithm (QBFT), the chain ID, and a list of pre-funded accounts. Besu does the rest.

The Config Input File

qbft-config.json JSON
{
  "genesis": {
    "config": {
      "chainId": 1337,
      "londonBlock": 0,
      "qbft": {
        "blockperiodseconds": 2,
        "epochlength": 30000,
        "requesttimeoutseconds": 4
      }
    },
    "nonce": "0x0",
    "gasLimit": "0x1fffffffffffff",
    "difficulty": "0x1",
    "alloc": {
      "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73": {
        "balance": "1000000000000000000000"
      }
    }
  },
  "blockchain": {
    "nodes": {
      "generate": true,
      "count": 4
    }
  }
}

2. Run the Generator

A single command turns that input into a full network configuration. You get a genesis.json and a folder per validator containing its private key, public key, and address.

Generate config Bash
# Produces ./networkFiles/genesis.json and ./networkFiles/keys/*
besu operator generate-blockchain-config \
  --config-file=qbft-config.json \
  --to=networkFiles \
  --private-key-file-name=key

3. Compose the 4-Node Network

Now wire up the nodes with Docker Compose. Node 1 exposes 8545 as the public RPC endpoint (the one you point Hardhat at). Nodes 2–4 join it via P2P. All four are validators in the QBFT set.

docker-compose.yml YAML
# 4-node Besu QBFT private network
services:
  besu-node1:
    image: hyperledger/besu:latest
    volumes:
      - ./networkFiles/genesis.json:/config/genesis.json
      - ./networkFiles/keys/node1:/opt/besu/keys
    ports:
      - "8545:8545"
    command:
      - --genesis-file=/config/genesis.json
      - --node-private-key-file=/opt/besu/keys/key
      - --rpc-http-enabled
      - --rpc-http-host=0.0.0.0
      - --rpc-http-api=ETH,NET,QBFT,WEB3,ADMIN
      - --host-allowlist=*
      - --min-gas-price=0

  besu-node2:
    image: hyperledger/besu:latest
    volumes:
      - ./networkFiles/genesis.json:/config/genesis.json
      - ./networkFiles/keys/node2:/opt/besu/keys
    command:
      - --genesis-file=/config/genesis.json
      - --node-private-key-file=/opt/besu/keys/key
      - --bootnodes=enode://<node1-pubkey>@besu-node1:30303
      - --min-gas-price=0

  besu-node3:
    image: hyperledger/besu:latest
    volumes:
      - ./networkFiles/genesis.json:/config/genesis.json
      - ./networkFiles/keys/node3:/opt/besu/keys
    command:
      - --genesis-file=/config/genesis.json
      - --node-private-key-file=/opt/besu/keys/key
      - --bootnodes=enode://<node1-pubkey>@besu-node1:30303
      - --min-gas-price=0

  besu-node4:
    image: hyperledger/besu:latest
    volumes:
      - ./networkFiles/genesis.json:/config/genesis.json
      - ./networkFiles/keys/node4:/opt/besu/keys
    command:
      - --genesis-file=/config/genesis.json
      - --node-private-key-file=/opt/besu/keys/key
      - --bootnodes=enode://<node1-pubkey>@besu-node1:30303
      - --min-gas-price=0

4. Start the Network

With the compose file in place, a single docker compose up -d spins up all four validators. Within a few seconds, they will elect a proposer, agree on the genesis, and start producing empty blocks. You now have a private Ethereum network on your laptop.

Start the network Bash
# Launch all four validators in the background
docker compose up -d

# Check the block number through node1 RPC
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  -H "Content-Type: application/json" \
  http://localhost:8545

Sanity Check

Point your browser or curl at http://localhost:8545 and call eth_blockNumber. You should see the block number increasing every couple of seconds. That is your QBFT network happily finalising empty blocks.

📦 Section 5

Deploying Contracts: Everything You Already Know

Here is the best part: your Hardhat project from Part 2 just works. There is no “Besu SDK,” no special compiler, no proprietary library. You add one network entry to hardhat.config.ts, point it at localhost:8545, and you are live on your consortium chain.

🔁

Same Solidity, Same Hardhat, Same Viem

Solidity 0.8.x compiles to the same bytecode. Hardhat Ignition deploys with the same buildModule API. viem's createPublicClient and createWalletClient talk to the Besu RPC the same way they talk to Anvil or Sepolia. Even MetaMask can add localhost:8545 as a custom network and sign transactions against your Besu chain.

The hardhat.config.ts Diff

The only real change is a new entry in the networks block. Chain ID 1337 must match the chainId in your QBFT genesis file. Accounts are the pre-funded addresses from your config (ETH preset): Besu has no concept of gas faucets on a private chain, you simply seed addresses in the genesis.

hardhat.config.ts TypeScript
// hardhat.config.ts
import type { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
  solidity: "0.8.24",
  networks: {
    besu: {
      url: "http://localhost:8545",
      chainId: 1337,
      accounts: [
        "0x8f2a559490...pre-funded-from-genesis"
      ],
      gasPrice: 0
    }
  }
};

export default config;

Sample Contract — Vault

A minimal Vault contract that lets a depositor lock ETH for a given withdrawal time. This is deliberately simple — the point is to show that your Part 2 Solidity workflow is untouched.

Vault.sol Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Vault {
    mapping(address => uint256) public balances;
    mapping(address => uint256) public unlockAt;

    event Deposited(address indexed who, uint256 amount, uint256 unlock);
    event Withdrawn(address indexed who, uint256 amount);

    function deposit(uint256 lockSeconds) external payable {
        require(msg.value > 0, "zero deposit");
        balances[msg.sender] += msg.value;
        unlockAt[msg.sender] = block.timestamp + lockSeconds;
        emit Deposited(msg.sender, msg.value, unlockAt[msg.sender]);
    }

    function withdraw() external {
        require(block.timestamp >= unlockAt[msg.sender], "still locked");
        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0;
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "transfer failed");
        emit Withdrawn(msg.sender, amount);
    }
}

Deploy with Ignition

Exactly the same Ignition module and the same CLI command as in Module 14 — the only difference is the --network besu flag.

Deploy Vault Bash
# ignition/modules/Vault.ts
// import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
// export default buildModule("VaultModule", (m) => {
//   const vault = m.contract("Vault");
//   return { vault };
// });

# Deploy to your Besu network
npx hardhat ignition deploy ignition/modules/Vault.ts --network besu
💡

Takeaway

If you have shipped one Solidity contract in Part 2, you can ship any Solidity contract on Besu. The enterprise-specific complexity is in operating the network, not in writing code.

🔐 Section 6

Permissioning

In a public Ethereum network, anyone can run a node and anyone can submit transactions from any address. That is a feature on mainnet and a bug in a regulated consortium. Besu's permissioning system lets you control exactly who can connect to your network and who can transact on it.

🛰️

Node Permissioning

Node permissioning restricts which peers can join the P2P network. Each participating node's enode URL (public key + IP + port) must appear on an allowlist, either configured locally on each node or — better — stored in a smart contract on the chain itself. Unknown enodes are simply refused the handshake.

👤

Account Permissioning

Account permissioning restricts which externally-owned accounts can submit transactions. A signed transaction from a non-allowed address is rejected before it enters the mempool. This is how you enforce KYC / onboarding rules at the chain level, not just at the dapp level.

📜

Smart-Contract-Based Permissioning

The most powerful mode is on-chain permissioning: both the node and account allowlists live in upgradeable smart contracts deployed to the chain itself. Consortium governance then becomes a matter of sending transactions to these contracts — adding a new bank means calling addNode(), removing one means calling removeNode(). Audit trails are built-in.

📄

Configuration File

Local permissioning is enabled through a permissions_config.toml file passed to Besu at startup. It lists allowed enodes, allowed accounts, and whether the lists should be enforced strictly. Combine it with the --permissions-nodes-config-file-enabled and --permissions-accounts-config-file-enabled flags.

⚖️

Why It Matters

Permissioning is what turns Besu from “a private Ethereum client” into a compliance-ready enterprise chain. Regulators can inspect the allowlists. Legal can point to the governance contract. Ops can add and remove members with a single transaction. This is something public Ethereum simply cannot offer.

🕶️ Section 7

Private Transactions

Even in a permissioned consortium, you rarely want every member to see every transaction. Two banks negotiating a bilateral deal do not want a third bank watching the numbers. Besu solves this with privacy groups, backed by a companion component called Tessera.

🧩

Tessera: The Privacy Sidecar

Tessera is an off-chain private transaction manager that runs as a sidecar next to each Besu node. It holds a separate private state database and exchanges encrypted payloads peer-to-peer with other Tesseras. Besu delegates anything marked as private to its local Tessera instance.

👥

Privacy Groups

A privacy group is a subset of the consortium — for example, “Bank A, Bank B, Bank C” — that can share private state. Transactions sent to a privacy group are encrypted end-to-end between the Tesseras of those members. A hash of the encrypted payload still gets recorded on the public chain, so ordering and integrity are guaranteed, but only members of the group can decrypt and execute the actual transaction.

📂

Public State vs. Private State

Each Besu node maintains two state tries: the public state (visible to everyone) and one private state per privacy group it participates in. A contract deployed privately only exists in the private state of its group. Other members do not even know the contract is there.

🎯

The privateFrom / privateFor Pattern

A private transaction adds three fields to a regular Ethereum transaction: privateFrom (your Tessera public key), privateFor (the array of recipients' Tessera public keys), and restriction: "restricted" (the payload is shared only with listed recipients). Besu's EEA-compliant RPC endpoints (eea_sendRawTransaction, priv_*) handle the rest.

💼

When to Use It

Privacy groups shine for bilateral financial instruments, confidential supply-chain data, competitive pricing between partners, and any scenario where consortium members are also competitors. They are the main reason enterprise teams pick Besu over public Ethereum L2s.

Killer Feature

The combination of EVM compatibility + BFT finality + permissioning + privacy groups is what makes Besu a genuine enterprise platform, not just “a private Geth.” This is the feature set Fabric competes against in the consortium market.

📝 Section 8

Exercise & Self-Check

Time to get your hands dirty. The exercises below walk you from zero to a running Besu network with a deployed contract and a permissioning rule. Do at least the first three on real hardware — the muscle memory matters.

Exercises

  1. 1. Run a 4-node QBFT network. Using the qbft-config.json and docker-compose.yml from Section 4, spin up a real local Besu network. Call eth_blockNumber every few seconds with curl or Postman and watch it increase. Kill one node (docker stop besu-node4) and confirm the other three still finalise blocks. Kill a second one and observe what happens. Explain the result in terms of (N − 1) / 3.
  2. 2. Deploy the Vault contract. Take the Vault.sol from Section 5, add a besu network entry to your hardhat.config.ts, write an Ignition module, and deploy it to your running network. Send a deposit from viem, read the balance back, and verify on-chain with eth_getStorageAt. Celebrate — you just shipped a Solidity contract to an enterprise chain.
  3. 3. Add a permissioning rule. Create a permissions_config.toml that allows only your own account to submit transactions, enable it in docker-compose.yml, restart the network, and try to send a tx from a different account. Document the exact error message you get. Then add the second account to the allowlist and retry.
  4. 4. Design a privacy group. On paper, sketch a 5-member consortium (A, B, C, D, E) where Bank A needs to send a private transaction to Banks B and C only. List: the Tessera keys involved, the privateFrom and privateFor fields, who sees what in the public state, and who sees what in the private state. You do not need to run Tessera for this — reasoning on paper is enough.
  5. 5. IBFT 2.0 vs QBFT trade-offs. In 200 words, explain to an engineering manager why you would pick QBFT over IBFT 2.0 for a brand new deployment in 2025. Cover liveness, view changes, specification, and backwards compatibility. End with one concrete scenario where you would still keep IBFT 2.0.

Self-Check

  1. Can you generate a Besu genesis file with besu operator generate-blockchain-config and explain each field in its input?
  2. Can you spin up a 4-node QBFT network and verify that it tolerates exactly one Byzantine validator?
  3. Can you deploy a Solidity contract to Besu using exactly the same Hardhat workflow you learned in Part 2, and explain which config lines change?
  4. Can you describe the difference between node permissioning and account permissioning, and when you would use an on-chain allowlist instead of a file?
  5. Can you explain, in your own words, how Tessera and privacy groups keep a transaction visible to some members and invisible to others?
Coming Up — Module 18

You have just built and operated a real enterprise Ethereum network. In Module 18 we pivot to Hyperledger Fabric, the other big player in the consortium space. Fabric's architecture is radically different — no EVM, no Solidity, chaincode in Go/JavaScript, channels for privacy, MSPs for identity. That module is conceptual by design: setting up Fabric is a course of its own, and the goal is for you to understand the model so that you can choose wisely when Besu is not the right fit.

Module 18: Fabric Conceptual →