Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
FROM fedora:39

# NOTE: ethereum-package uses ServiceConfig(publish_udp=...). This requires Kurtosis engine >= 1.15.2.
# If you upgrade Kurtosis, restart the engine to pick up the new Starlark API:
# kurtosis engine restart

ARG KURTOSIS_CLI_VERSION=1.15.2
# Install necessary packages
RUN dnf update -y && \
dnf install -y \
Expand All @@ -18,7 +23,7 @@ RUN dnf update -y && \

# Install Kurtosis CLI via RPM repository
RUN echo -e '[kurtosis]\nname=Kurtosis\nbaseurl=https://yum.fury.io/kurtosis-tech/\nenabled=1\ngpgcheck=0' | tee /etc/yum.repos.d/kurtosis.repo && \
dnf install -y kurtosis-cli && \
dnf install -y "kurtosis-cli-${KURTOSIS_CLI_VERSION}-*" && \
dnf clean all

# Set working directory
Expand Down
79 changes: 47 additions & 32 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,71 +1,86 @@
ENCLAVE_NAME=localnet
PARAMS_FILE=params.yaml
SSV_NODE_COUNT?=4
ENCLAVE_NAME?=localnet
SSV_COMMIT?=stage
# ssv-mini Makefile

# Core params
ENCLAVE_NAME ?= localnet
PARAMS_FILE ?= params.yaml
SSV_NODE_COUNT ?= 4
SSV_COMMIT ?= stage
KURTOSIS_MIN_VERSION ?= 1.15.2

# Repos and refs (override as needed)
SSV_REPO ?= https://github.com/ssvlabs/ssv.git
SSV_REF ?= $(SSV_COMMIT)

ANCHOR_REPO ?= https://github.com/sigp/anchor.git
ANCHOR_REF ?= unstable

default: run-with-prepare

# Run with prepare: Downloads latest repos (ssv stage, anchor unstable, ethereum2-monitor main) and builds Docker images
.PHONY: run-with-prepare
run-with-prepare: prepare
.PHONY: default run-with-prepare run reset-with-prepare reset clean show restart-ssv-nodes prepare check-kurtosis

check-kurtosis:
@bash scripts/check-kurtosis-version.sh "$(KURTOSIS_MIN_VERSION)"

# Run with prepare: clone/update repos and build images
run-with-prepare: check-kurtosis prepare
kurtosis run --verbosity DETAILED --enclave ${ENCLAVE_NAME} . "$$(cat ${PARAMS_FILE})"

# Run without prepare: Uses existing local repos and Docker images (for custom branches/versions)
.PHONY: run
run:
# Run without prepare: use existing repos and images
run: check-kurtosis
kurtosis run --verbosity DETAILED --enclave ${ENCLAVE_NAME} . "$$(cat ${PARAMS_FILE})"

# Reset with prepare: Clean and run with latest repos and fresh Docker images
.PHONY: reset-with-prepare
reset-with-prepare: prepare
# Reset with prepare: clean and run fresh
reset-with-prepare: check-kurtosis prepare
kurtosis clean -a
kurtosis run --enclave ${ENCLAVE_NAME} . "$$(cat ${PARAMS_FILE})"

# Reset without prepare: Clean and run with existing local repos and Docker images
.PHONY: reset
reset:
# Reset without prepare: clean and run with existing assets
reset: check-kurtosis
kurtosis clean -a
kurtosis run --enclave ${ENCLAVE_NAME} . "$$(cat ${PARAMS_FILE})"

.PHONY: clean
clean:
kurtosis clean -a

.PHONY: show
show:
kurtosis enclave inspect ${ENCLAVE_NAME}

.PHONY: restart-ssv-nodes
restart-ssv-nodes:
@echo "Updating SSV Node services. Count: $(SSV_NODE_COUNT) ..."
@for i in $(shell seq 0 $(shell expr $(SSV_NODE_COUNT) - 1)); do \
echo "Updating service: ssv-node-$$i"; \
kurtosis service update $(ENCLAVE_NAME) ssv-node-$$i; \
done

.PHONY: prepare
prepare:
@echo "⏳ Preparing requirements..."

# SSV (public)
@if [ ! -d "../ssv" ]; then \
git clone https://github.com/ssvlabs/ssv.git ../ssv; \
echo "Cloning SSV..."; \
git clone "$(SSV_REPO)" ../ssv; \
else \
echo "✅ ssv repo already cloned."; \
cd ../ssv && git fetch && git checkout ${SSV_COMMIT}; \
cd ../ssv && \
git remote set-url origin "$(SSV_REPO)" && \
git fetch --all --tags && \
git checkout "$(SSV_REF)" && \
git pull origin "$(SSV_REF)"; \
fi
@docker image inspect node/ssv >/dev/null 2>&1 || (cd ../ssv && docker build -t node/ssv . && echo "✅ SSV image built successfully.")

# Anchor (public)
@if [ ! -d "../anchor" ]; then \
git clone https://github.com/sigp/anchor.git ../anchor; \
echo "Cloning Anchor..."; \
git clone "$(ANCHOR_REPO)" ../anchor; \
else \
echo "✅ anchor repo already cloned."; \
cd ../anchor && git fetch && git checkout unstable; \
cd ../anchor && \
git remote set-url origin "$(ANCHOR_REPO)" && \
git fetch --all --tags && \
git checkout "$(ANCHOR_REF)" && \
git pull origin "$(ANCHOR_REF)"; \
fi
@docker image inspect node/anchor >/dev/null 2>&1 || (cd ../anchor && docker build -f Dockerfile.devnet -t node/anchor . && echo "✅ Anchor image built successfully.")
@if [ ! -d "../ethereum2-monitor" ]; then \
git clone https://github.com/ssvlabs/ethereum2-monitor.git ../ethereum2-monitor; \
else \
echo "✅ ethereum2-monitor repo already cloned."; \
cd ../ethereum2-monitor && git fetch && git checkout main; \
fi
@docker image inspect monitor >/dev/null 2>&1 || (cd ../ethereum2-monitor && docker build -t monitor . && echo "✅ Ethereum2 Monitor image built successfully.")

@echo "✅ All requirements are prepared, spinning up the enclave..."
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@ Kurtosis devnet for running local SSV networks.

## Prerequisites
- Docker
- [Kurtosis](https://docs.kurtosis.com/install)
- [Kurtosis](https://docs.kurtosis.com/install) (>= 1.15.2)

### Troubleshooting

If you see:

```
ServiceConfig: unexpected keyword argument "publish_udp"
```

Your Kurtosis engine is too old. Upgrade Kurtosis to >= 1.15.2 and restart the engine:

```bash
kurtosis engine restart
```

NOTE: Restarting the engine will stop running enclaves/services.

## Quick Start (Recommended)
The easiest way to get started is using the automated setup with the `prepare` command:
Expand Down Expand Up @@ -128,6 +144,17 @@ Use this if you want to shutdown previous network and start one from genesis usi
make reset
```

## Unregistered Validator Keystores (Uniform Password)

Unregistered validators are generated with a single uniform password to simplify testing.

- Configure the password in `params.yaml` under:
- `extra_params.unregistered_validator_password: "password"`
- Only unregistered validator keystores are affected. Registered validators keep using eth2-val-tools defaults.
- Verification (after `make run-with-prepare`):
- `kurtosis service exec localnet validator-key-generation-ssv-validator-keystore-unregistered -- sh -lc 'head -n1 /node-keystores-unregistered/secrets/* | sort -u'`
- Expect exactly one line (the uniform password).

### Goals

- Anyone can run a SSV network on their pc
Expand All @@ -136,4 +163,4 @@ make reset
- Possible to scale by adding resources

## Architecture
![Architecture](./docs/architecture.png)
![Architecture](./docs/architecture.png)
5 changes: 4 additions & 1 deletion contract/registration/RegisterValidators.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ contract RegisterValidator is Script {
SSVNetwork public ssvNetwork;

// Initial deposit amount (adjust as needed)
uint256 constant DEPOSIT_AMOUNT = 1 ether;
// Increased to 15 ETH to ensure sufficient balance for reactivation after liquidation
// For ~33 validators, minimum required is ~7.26 ETH based on:
// minimumBlocksBeforeLiquidation × (burnRate + networkFee) × validatorCount
uint256 constant DEPOSIT_AMOUNT = 15 ether;

function run(address ssvNetworkAddress, bytes[] memory publicKeys, bytes[] memory sharesDatas, uint64[] memory operatorIds) external {
ssvNetwork = SSVNetwork(ssvNetworkAddress);
Expand Down
108 changes: 91 additions & 17 deletions generators/validator-keygen.star
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR = "/node-keystores"
NODE_UNREGISTERED_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR = "/node-keystores-unregistered"

PRYSM_PASSWORD = "password"
PRYSM_PASSWORD_FILEPATH_ON_GENERATOR = "/tmp/prysm-password.txt"

KEYSTORES_GENERATION_TOOL_NAME = "/app/eth2-val-tools"

ETH_VAL_TOOLS_IMAGE = "protolambda/eth2-val-tools:latest"

SUCCESSFUL_EXEC_CMD_EXIT_CODE = 0

RAW_KEYS_DIRNAME = "keys"
Expand All @@ -20,48 +19,123 @@ TEKU_SECRETS_DIRNAME = "teku-secrets"

KEYSTORE_GENERATION_FINISHED_FILEPATH_FORMAT = "/tmp/keystores_generated-{0}-{1}"

SERVICE_NAME = "validator-key-generation-ssv-validator-keystore"
SERVICE_NAME_REGSITER = "validator-key-generation-ssv-validator-keystore"
SERVICE_NAME_UNREGSITER = "validator-key-generation-ssv-validator-keystore-unregistered"

ENTRYPOINT_ARGS = [
"sleep",
"99999",
]

SERVICE_CONFIG = ServiceConfig(
image=ETH_VAL_TOOLS_IMAGE,
entrypoint=ENTRYPOINT_ARGS,
files={},
)

ARTIFACT_PREFIX = 'ssv-validators'


def generate_validator_keystores(plan, mnemonic, start_index, validator_count):
plan.add_service(SERVICE_NAME, SERVICE_CONFIG)
def generate_validator_keystores(plan, mnemonic, start_index, validator_count, eth_val_tools_image, register=True):
if register:
service_name = SERVICE_NAME_REGSITER
output_dirpath = NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR
else:
service_name = SERVICE_NAME_UNREGSITER
output_dirpath = NODE_UNREGISTERED_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR

service_config = ServiceConfig(
image=eth_val_tools_image,
entrypoint=ENTRYPOINT_ARGS,
files={},
)
plan.add_service(service_name, service_config)

stop_index = start_index + validator_count

generate_keystores_cmd = '{0} keystores --insecure --prysm-pass {1} --out-loc {2} --source-mnemonic "{3}" --source-min {4} --source-max {5}'.format(
KEYSTORES_GENERATION_TOOL_NAME,
PRYSM_PASSWORD,
NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR,
output_dirpath ,
mnemonic,
start_index,
stop_index,
)
teku_permissions_cmd = (
"chmod 0777 -R " + NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR + "/" + TEKU_KEYS_DIRNAME
"chmod 0777 -R " + output_dirpath + "/" + TEKU_KEYS_DIRNAME
)
raw_secret_permissions_cmd = (
"chmod 0600 -R " + NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR + "/" + RAW_SECRETS_DIRNAME
"chmod 0600 -R " + output_dirpath + "/" + RAW_SECRETS_DIRNAME
)

all_sub_command_strs = [generate_keystores_cmd, teku_permissions_cmd, raw_secret_permissions_cmd]

command_str = " && ".join(all_sub_command_strs)

command_result = plan.exec(
recipe=ExecRecipe(command=["sh", "-c", command_str]), service_name=SERVICE_NAME
recipe=ExecRecipe(command=["sh", "-c", command_str]), service_name=service_name
)
plan.verify(command_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)

artifact_name = "{0}-{1}-{2}".format(
ARTIFACT_PREFIX,
start_index,
stop_index - 1,
)
artifact_name = plan.store_service_files(
service_name, output_dirpath , name=artifact_name
)

base_dirname_in_artifact = path_base(output_dirpath )
keystore_files = struct(
files_artifact_uuid=artifact_name,
raw_root_dirpath=path_join(base_dirname_in_artifact),
raw_keys_relative_dirpath=path_join(base_dirname_in_artifact, RAW_KEYS_DIRNAME),
raw_secrets_relative_dirpath=path_join(base_dirname_in_artifact, RAW_SECRETS_DIRNAME),
nimbus_keys_relative_dirpath=path_join(base_dirname_in_artifact, NIMBUS_KEYS_DIRNAME),
prysm_relative_dirpath=path_join(base_dirname_in_artifact, PRYSM_DIRNAME),
teku_keys_relative_dirpath=path_join(base_dirname_in_artifact, TEKU_KEYS_DIRNAME),
teku_secrets_relative_dirpath=path_join(base_dirname_in_artifact, TEKU_SECRETS_DIRNAME),
)

return keystore_files


def generate_unregistered_validator_keystores_uniform(plan, mnemonic, start_index, validator_count, password, lighthouse_image):
service_name = SERVICE_NAME_UNREGSITER
output_dirpath = NODE_UNREGISTERED_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR

# Start a dormant Lighthouse container to generate EIP-2335 keystores with a uniform password
plan.add_service(service_name, ServiceConfig(
image=lighthouse_image,
entrypoint=["tail", "-f", "/dev/null"],
env_vars={
"MNEMONIC": mnemonic,
"UNIFORM_PASSWORD": password,
},
))

stop_index = start_index + validator_count

# Generate validators into Lighthouse's default structure under output_dirpath
# Then align directories to match expected structure (keys + secrets)
gen_cmd = (
"mkdir -p {out} {out}/vm && "
+ "printf '%s\n' \"$MNEMONIC\" > /tmp/mnemonic.txt && "
+ "printf '%s\n' \"$UNIFORM_PASSWORD\" > /tmp/pw && "
+ "cat /tmp/pw | lighthouse validator_manager create "
+ "--output-path {out}/vm "
+ "--first-index {start} --count {count} "
+ "--mnemonic-path /tmp/mnemonic.txt "
+ "--stdin-inputs --specify-voting-keystore-password "
+ "--disable-deposits --force-bls-withdrawal-credentials && "
+ "(command -v jq >/dev/null 2>&1 || (apk add --no-cache jq) || (apt-get update -y && apt-get install -y jq) || (apt update -y && apt install -y jq)) && "
+ "mkdir -p {out}/keys {out}/secrets && "
+ "jq -c '.[]' {out}/vm/validators.json | while read -r item; do "
+ "pub=$(echo \"$item\" | jq -r '.voting_keystore | fromjson | .pubkey'); "
+ "pass=$(echo \"$item\" | jq -r '.voting_keystore_password'); "
+ "pk=0x${{pub#0x}}; mkdir -p {out}/keys/$pk; "
+ "echo \"$pass\" > {out}/secrets/$pk; "
+ "echo \"$item\" | jq -r '.voting_keystore' > {out}/keys/$pk/voting-keystore.json; "
+ "done"
).format(out=output_dirpath, start=str(start_index), count=str(validator_count))

command_result = plan.exec(
recipe=ExecRecipe(command=["sh", "-c", gen_cmd]), service_name=service_name
)
plan.verify(command_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)

Expand All @@ -71,10 +145,10 @@ def generate_validator_keystores(plan, mnemonic, start_index, validator_count):
stop_index - 1,
)
artifact_name = plan.store_service_files(
SERVICE_NAME, NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR, name=artifact_name
service_name, output_dirpath, name=artifact_name
)

base_dirname_in_artifact = path_base(NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR)
base_dirname_in_artifact = path_base(output_dirpath)
keystore_files = struct(
files_artifact_uuid=artifact_name,
raw_root_dirpath=path_join(base_dirname_in_artifact),
Expand Down
Loading