# **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)