---
# System prepended metadata

title: '**How to Set Up an SSV Network Devnet**'

---

# **How to Set Up an SSV Network Devnet**

## **Prerequisites**
- [Beacon Chain](https://github.com/ethpandaops/ethereum-package) Set up the Beacon Node environment using Kurtosis v1.4.1
- [SSV Network](https://github.com/ssvlabs/ssv-network) v1.2.0
- [SSV Node](https://github.com/ssvlabs/ssv) v2.1.0
- [SSV DKG](https://github.com/ssvlabs/ssv-dkg) v2.1.0
- [SSV Spec](https://github.com/ssvlabs/ssv-spec) v0.3.7-b

Example file from kurtosis customize network can be retrieved from:
https://github.com/slotheth/kurtosis-network-config


## **Step 1 - Beacon chain (Kurtosis)**
Install the Kurtosis CLI ollow the installation guide on the [kurtosis](https://docs.kurtosis.com/install).
Prepare the network configuration copy the example `network.yaml` file and customize it as needed for setup.

Check Available Versions and Install Specific Version v1.4.1:
```
apt list -a kurtosis-cli
apt install kurtosis-cli=1.4.1
```

Clean up existing beacon chain data:
```
kurtosis clean -a
```

Initialize the beacon chain with configuration file:
```
kurtosis run --enclave <YOUR_DEVNET_NAME> github.com/ethpandaops/ethereum-package@861cf2ec532efe6cc2496f53af192826607e7524 --args-file <YOUR_PATH_CONFIG>/network.yaml
```
Replace `<YOUR_DEVNET_NAME>` with the desired enclave name and `<YOUR_PATH_CONFIG>` with the path to your customized `network.yaml` file. The Kurtosis package used in this setup corresponds to commit `861cf2ec532efe6cc2496f53af192826607e7524`.

Retrieve the list of services running in the beacon chain enclave:
```
kurtosis enclave inspect <YOUR_DEVNET_NAME>
```
![image](https://hackmd.io/_uploads/B17M-i9Pyg.png)

In this example, the Devnet environment includes the following components:
-  Consensus Layer (CL).
-  Execution Layer (EL).
-  Validator Client (VC).
![image](https://hackmd.io/_uploads/SknLDscwyx.png)


From this setup, we will have explorers available for use in the next step:
- Dora: `c28fbd348b22   dora                                             http: 8080/tcp -> http://127.0.0.1:32821`
- BlockScout: `3d8a706c5307   blockscout                                       http: 4000/tcp -> http://127.0.0.1:32819`


We can create new Consensus Layer (CL) and Execution Layer (EL) nodes and have them join the main devnet network.
![image](https://hackmd.io/_uploads/HkxvAscDkg.png)

Download the network configuration files from your `apache` devnet.
- Network Configs: `5288c99f3ff3   apache                                           http: 80/tcp -> http://127.0.0.1:32822`
![image](https://hackmd.io/_uploads/r1qgX25DJg.png)

We can find the **MinGenesisTime**  value to use with the **SSV Spec** in the `genesis.json` file within the network configs. The value will be listed under the key **timestamp** or We can also check the **Genesis Time** using the Dora Explorer and convert to Unix timestamp.


Create a docker-compose file and add the devnet network environment in two sections:
- networks: kt-<YOUR_KURTOSIS_DOCKER_NETWORK_NAME>
- the command from `start-nethermind.sh`

We can check the Kurtosis Docker network:
```
docker network ls
```

> /docker-compose.yml
```yaml {numberLines}
  el-nethermind:
    networks:
      kt-<YOUR_DOCKER_KURTOSIS_NETWORK_NAME>:
    image: nethermindeth/nethermind:master
    container_name: el-nethermind
    environment:
      - PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
      - APP_UID=1654
      - ASPNETCORE_HTTP_PORTS=8080
      - DOTNET_RUNNING_IN_CONTAINER=true
      - DOTNET_VERSION=9.0.0
      - ASPNET_VERSION=9.0.0
    entrypoint: /network-configs/start-nethermind.sh
    ports:
      - "8575:8575"   # HTTP RPC
      - "8565:8565"   # WebSocket RPC
      - "8553:8553"   # AuthRPC
      - "30305:30305/tcp" # P2P Port
      - "30305:30305/udp"
      - "9003:9003"   # Metrics Port
    volumes:
      - ./network-configs:/network-configs
      - ./data/nethermind:/data
      - ./jwt:/jwt
    restart: unless-stopped
```

> /start-nethermind.sh
```shell {numberLines}
#!/bin/sh

# Ensure required directories exist
echo "Ensuring required directories exist..."
mkdir -p /data/nethermind/execution-data /data/nethermind-logs

# Copy the chain spec file
echo "Copying the chain spec file..."
cp /network-configs/chainspec.json /data/nethermind/execution-data/chainspec.json

# Start Nethermind with the new port mappings
echo "Starting Nethermind with updated port mappings..."
/nethermind/nethermind \
  --log=INFO \
  --datadir=/data/nethermind/execution-data \
  --Init.WebSocketsEnabled=true \
  --JsonRpc.Enabled=true \
  --JsonRpc.EnabledModules=net,eth,consensus,subscribe,web3,admin,debug,trace,txpool \
  --JsonRpc.Host=0.0.0.0 \
  --JsonRpc.Port=8575 \
  --JsonRpc.WebSocketsPort=8565 \
  --JsonRpc.EngineHost=0.0.0.0 \
  --JsonRpc.EnginePort=8553 \
  --Network.ExternalIp=<YOUR_DOCKER_INTERNAL_IP> \
  --Network.DiscoveryPort=30305 \
  --Network.P2PPort=30305 \
  --JsonRpc.JwtSecretFile=/jwt/jwtsecret \
  --Metrics.Enabled=true \
  --Metrics.ExposePort=9003 \
  --Metrics.ExposeHost=0.0.0.0 \
  --config=none \
  --Init.ChainSpecPath=/data/nethermind/execution-data/chainspec.json \
  --Discovery.Bootnodes=<YOUR_DEVNET_BOOTNODES> # bootnodes can retrieve from network-config/bootnode.txt
```

Run the Execution Layer (EL) node to connect the Devnet network:
```
docker-compose up -d
```


## **Step 2 - SSV Network Smart Contract**
Clone [SSV Network](https://github.com/ssvlabs/ssv-network) repository from GitHub and check out version v1.2.0:
```
git clone --branch v1.2.0 --depth 1 https://github.com/ssvlabs/ssv-network.git
```
Edit the file `hardhat.config.ts` to configure the network deployment:
-  Import `defineChain` to set a custom configuration for your Beacon Chain.
-  Use the Kurtosis chain ID: `3151908`.
-  For the Explorer URL, this guide uses Blockscout provided by Kurtosis.
> /hardhat.config.ts
```typescript {numberLines}
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-ethers";
import "@openzeppelin/hardhat-upgrades";
import dotenv from "dotenv";
import { defineChain } from "viem";

import "./tasks/deploy";

const networkName = "kurtosis";
const chainId = <KURTOSIS_CHAIN_ID>;
const rpcURL = <YOUR_NODE_RPC_URL>;
const pk = <YOUR_PRIVATE_KEY>;

export const devnetChain = defineChain({
  id: chainId,
  name: networkName,
  nativeCurrency: {
    decimals: 18,
    name: "Ether",
    symbol: "ETH",
  },
  rpcUrls: {
    default: {
      http: [rpcURL],
    },
  },
});

const config: HardhatUserConfig = {
  sourcify: {
    enabled: false,
  },
  etherscan: {
    apiKey: {
      kurtosis: "xxx",
    },
    customChains: [
      {
        network: networkName,
        chainId: chainId,
        urls: {
          apiURL: "<YOUR_EXPLORER_URL>/api",
          browserURL: "<YOUR_EXPLORER_URL>/",
        },
      },
    ],
  },
  solidity: {
    compilers: [
      { version: "0.8.4" },
      { version: "0.8.20" },
      { version: "0.8.24" },
      { version: "0.8.27" },
    ],
  },
  networks: {
    devnet: {
      url: rpcURL,
      chainId: chainId,
      accounts: pk ? [`0x${pk}`] : [],
    },
  },
};

export default config;
```

Deploy the SSV Network smart contract using Hardhat:
```
npx hardhat clean
npx hardhat compile
npx hardhat --network devnet deploy:all
```

To verify smart contract:
```
npx hardhat verify --network devnet <YOUR_CONTRACT_ADDRESS_TO_VERIFY>
```

## **Step 3 - SSV Node Operator**
Clone [SSV Node](https://github.com/ssvlabs/ssv) repository from GitHub and check out version v2.1.0:
```
git clone --branch v2.1.0 --depth 1 https://github.com/ssvlabs/ssv.git
```

Edit the file `networkconfig/holesky.go` to configure the network settings.
> /networkconfig/holesky.go
```go {numberLines}
package networkconfig

import (
	"math/big"

	spectypes "github.com/bloxapp/ssv-spec/types"

	"github.com/bloxapp/ssv/protocol/v2/blockchain/beacon"
)

var Holesky = NetworkConfig{
	Name:                 "holesky",
	Beacon:               beacon.NewNetwork(spectypes.HoleskyNetwork),
	Domain:               spectypes.DomainType{0x0, 0x0, 0x5, 0x1},
	GenesisEpoch:         1,
	RegistrySyncOffset:   new(big.Int).SetInt64(<YOUR_SSV_NETWORK_DEPLOYMENT_BLOCK_NUMBER>),
	RegistryContractAddr: "<YOUR_SSV_NETWORK_ADDRESS>", // replace address from your devnet beacon chain
	Bootnodes: []string{},
}

```

Update the SSV Spec reference to point to your desired path (e.g., a GitHub repository URL or a local path)
> /go.mod
```go {numberLines}
replace github.com/bloxapp/ssv-spec => <YOUR_SSV_SPEC_> // Specify the GitHub repo URL or local path
```

Copy the `config/config.example` file to `config/config.yaml` and update it with your configuration details.
> /config/config.yaml
```yaml {numberLines}
global:
  # Console log level (debug, info, warn, error, fatal, panic)
  LogLevel: info
  
  # Debug logs file path
  LogFilePath: ./data/debug.log

db:
  # Path to a persistent directory to store the node's database.
  Path: ./data/db

ssv:
  # The SSV network to join to
  # Mainnet = Network: mainnet (default)
  # Testnet = Network: jato-v2
  Network: holesky # We use the name holesky because we replace the value over the original config in /networkconfig/holesky.go of holesky network.

eth2:
  # HTTP URL of the Beacon node to connect to.
  BeaconNodeAddr: <YOUR_BEACON_NODE_RPC_URL>

eth1:
  # WebSocket URL of the Eth1 node to connect to.
  ETH1Addr: <YOUR_WEB_SOCKET_URL> # example ws://example.url:8546/ws

p2p:
  # Optionally specify the external IP address of the node, if it cannot be determined automatically.
  # HostAddress: 192.168.1.1

  # Optionally override the default TCP & UDP ports of the node.
  # TcpPort: 13001
  # UdpPort: 12001

# Note: Operator private key can be generated with the `generate-operator-keys` command.
KeyStore:
  PrivateKeyFile: /encrypted_private_key.json
  PasswordFile: /password

# This enables monitoring at the specified port, see https://github.com/bloxapp/ssv/tree/main/monitoring
MetricsAPIPort: 15000

# This enables the SSV API at the specified port. Refer to the documentation at https://bloxapp.github.io/ssv/
# It's recommended to keep this port private to prevent potential resource-intensive attacks.
# SSVAPIPort: 16000
```



## **Step 4 - SSV Spec**

Clone the [SSV Node Spec](https://github.com/ssvlabs/ssv-spec.git) repository from GitHub and check out version v0.3.7-b. This version corresponds to the `go.mod` file used in the SSV Node:
```
git clone --branch v0.3.7-b --depth 1 https://github.com/ssvlabs/ssv-spec.git
```

Edit the `types/beacon_types.go` file to configure the **MinGenesisTime** function.
> /types/beacon_types.go
```go {numberLines}
// MinGenesisTime returns min genesis time value
func (n BeaconNetwork) MinGenesisTime() uint64 {
	switch n {
	case MainNetwork:
		return 1606824023
	case HoleskyNetwork:
                return <YOUR_GENESIS_TIME> // replace genesis time from your beacon chain in Unix time
	case PraterNetwork:
		return 1616508000
	case BeaconTestNetwork:
		return 1616508000
	default:
		return 0
	}

}
```

## **Step 5 - SSV DKG**
Clone [SSV DKG](https://github.com/ssvlabs/ssv-dkg) repository from GitHub and check out version v2.1.0:
```
git clone --branch v2.1.0 --depth 1 https://github.com/ssvlabs/ssv-dkg.git
```

## **Step 6 - Deploying and Running the Node Operator**
After updating the SSV stack, you can prepare to run the SSV Operator and SSV DKG using either Docker images or by building binary files. We recommend reading the [Operator User Guides](https://docs.ssv.network/operator-user-guides/operator-node) for detailed instructions on setting up the operator.

As you know the number of cluster operators you select must be [3f+1](https://docs.ssv.network/developers/get-started) compatible - e.g. 4, 7, 10, 13…N
In this example, we will use Docker to run both the SSV Operator and SSV DKG.

We can add additional SSV nodes and SSV DKG instances as needed to meet the required quantity. If you are setting up multiple instances like this, **be cautious with the configuration of external ports to avoid conflicts**.


>/docker-compose.yml
```yaml {numberLines}
services:
  ssv:
    image: <YOUR_SSV_DOCKER_IMAGE>
    ports:
      - 13001:13001
      - 12001:12001/udp
      - 15000:15000
      - 16000:16000
    command:
        make BUILD_PATH="/go/bin/ssvnode" start-node
    volumes:
      - ./config.yaml:/config/config.yaml
      - ./data/node_1:/data
      - ./password:/password
      - ./encrypted_private_key.json:/encrypted_private_key.json
    environment:
      - CONFIG_PATH=/config/config.yaml
    container_name: ssv_node
    restart: unless-stopped
    networks:
      - ssv
  dkg:
    image: <YOUR_SSV_DKG_DOCKER_IMAGE>
    ports:
      - "3030:3030"
    command:
      ["start-operator", "--configPath", "/data/operator.yaml"]
    volumes:
      - ./data/dkg_1:/data
    networks:
      - ssv
      
 # Adding More Operators Based on the Number of Clusters Needed

networks:
  ssv:
    name: ssv
    driver: bridge
```


Run the operator node to connect to the Devnet network:
```
docker-compose up -d
```


In all examples, the Devnet environment includes the following components:
-  Consensus Layer.
-  Execution Layer.
-  Validator Client.
-  New Consensus Layer.
-  New Execution Layer.
-  SSV Operator.
-  SSV DKG.

![image](https://hackmd.io/_uploads/B1DCD8iP1e.png)


However, if check the logs from the SSV Node operator, you may notice the message: `got round change timeout`.

This issue can be resolved by adding **TrustedPeers** to the `config.yaml` file of the SSV Node Operator.

> /config/config.yaml
```yaml {numberLines}
global:
  # Console log level (debug, info, warn, error, fatal, panic)
  LogLevel: info
  
  # Debug logs file path
  LogFilePath: ./data/debug.log

db:
  # Path to a persistent directory to store the node's database.
  Path: ./data/db

ssv:
  # The SSV network to join to
  # Mainnet = Network: mainnet (default)
  # Testnet = Network: jato-v2
  Network: holesky # We use the name holesky because we replace the value over the original config in /networkconfig/holesky.go of holesky network.

eth2:
  # HTTP URL of the Beacon node to connect to.
  BeaconNodeAddr: <YOUR_BEACON_NODE_RPC_URL>

eth1:
  # WebSocket URL of the Eth1 node to connect to.
  ETH1Addr: <YOUR_WEB_SOCKET_URL> # example ws://example.url:8546/ws

p2p:
  # Optionally specify the external IP address of the node, if it cannot be determined automatically.
  # HostAddress: 192.168.1.1

  # Optionally override the default TCP & UDP ports of the node.
  TcpPort: 13001 # The TCP port for each instance should match the external ports defined in your Docker Compose configuration, for example, 13001-13004.
  UdpPort: 12001 # The UDP port for each instance should match the external ports defined in your Docker Compose configuration, for example, 12001-12004.
  TrustedPeers: ["<YOUR_TRUSTED_PEERS>"] # Add the trusted peers from each SSV Node Operator.
  
# Note: Operator private key can be generated with the `generate-operator-keys` command.
KeyStore:
  PrivateKeyFile: /encrypted_private_key.json
  PasswordFile: /password

# This enables monitoring at the specified port, see https://github.com/bloxapp/ssv/tree/main/monitoring
MetricsAPIPort: 15000
```


You can retrieve the trusted peers for each instance using the following command:
```
docker logs -f <YOUR_SSV_OPERATOR_CONTAINER_NAME> | grep trusted_peers
```

The result should display **my_address** and **trusted_peers** as follows:

```
2025-01-20T06:44:41.082271Z	INFO	P2PNetwork	starting p2p	{"my_address": "/ip4/127.0.0.1/tcp/13001/p2p/16Uiu2HAm3ymZiwvNFGAjFJwgP6EfyCcXKq7F3RNXfhLgExxxxxx,/ip4/172.19.0.3/tcp/13001/p2p/16Uiu2HAm3ymZiwvNFGAjFJwgP6EfyCcXKq7F3RNXfhLgErxxxxxx,/ip4/<YOUR_EXTERNAL_IP>/tcp/13001/p2p/16Uiu2HAm3ymZiwvNFGAjFJwgP6EfyCcXKq7F3RNXfhLgErxxxxxx", "trusted_peers": 4}
```

Copy this line for each operator and add it to the `config/config.yaml` file for each SSV Node Operator.
```
/ip4/<YOUR_EXTERNAL_IP>/tcp/13001/p2p/16Uiu2HAm3ymZiwvNFGAjFJwgP6EfyCcXKq7F3RNXfhLgErxxxxxx
```

Example **TrustedPeers** from SSV Node Operator 1
> /config/config.yaml
```yaml {numberLines}
p2p:
  # Optionally specify the external IP address of the node, if it cannot be determined automatically.
  # HostAddress: 192.168.1.1

  # Optionally override the default TCP & UDP ports of the node.
  TcpPort: 13001 # The TCP port for each instance should match the external ports defined in your Docker Compose configuration, for example, 13001-13004.
  UdpPort: 12001 # The UDP port for each instance should match the external ports defined in your Docker Compose configuration, for example, 12001-12004.
  TrustedPeers: ["/ip4/<YOUR_EXTERNAL_IP_OPERATOR_2>/tcp/13002/p2p/16Uiu2HAm3ymZiwvNFGAjFJwgP6EfyCcXKq7F3RNXfhLgErMMTwwe","/ip4/<YOUR_EXTERNAL_IP_OPERATOR_3>/tcp/13003/p2p/16Uiu2HAm3ymZiwvNFGAjFJwgP6EfyCcXKq7F3RNXfhLgErMMTwwe","/ip4/<YOUR_EXTERNAL_IP_OPERATOR_4>/tcp/13004/p2p/16Uiu2HAm3ymZiwvNFGAjFJwgP6EfyCcXKq7F3RNXfhLgErMMTwwe"] # Add the trusted peers from each SSV Node Operator.
```
After that, run `docker-compose up -d` again.


## **Conclusion and Precautions**
- SSV Node Operator and SSV DKG If you want to avoid complex configurations such as Docker Compose port, server reverse proxies, and other settings, I recommend setting up each operator on a separate server.
- In this example, we continue using Network = **holesky** in `/config/config.yaml` on the Devnet, as we aim to minimize modifications to the configuration as much as possible.
- If you are using a different version than the one mentioned in this example, you should review the changes in your current version, as some CLI commands may no longer be available.
- This example does not cover how to register an operator or deposit a validator. You can find this information in the [SSV Smart Contracts Documentation.](https://docs.ssv.network/developers/smart-contracts/ssvnetwork)


## **Resources and Official Documentation**
- [SSV Network](https://docs.ssv.network/)
- [Kurtosis](https://docs.kurtosis.com/)
- [Hardhat](https://hardhat.org/hardhat-network/docs/overview) 
