Commit 91029acf authored by Tedi Mitiku's avatar Tedi Mitiku Committed by GitHub

feat: use eth-network-package to spin up participant network (#90)

parent 41e3d2cd
participant_network = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/participant_network.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
static_files = import_module("github.com/kurtosis-tech/eth2-package/src/static_files/static_files.star")
genesis_constants = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/genesis_constants/genesis_constants.star")
genesis_constants = import_module("github.com/kurtosis-tech/eth-network-package/src/prelaunch_data_generator/genesis_constants/genesis_constants.star")
eth_network_module = import_module("github.com/kurtosis-tech/eth-network-package/main.star")
transaction_spammer = import_module("github.com/kurtosis-tech/eth2-package/src/transaction_spammer/transaction_spammer.star")
forkmon = import_module("github.com/kurtosis-tech/eth2-package/src/forkmon/forkmon_launcher.star")
prometheus = import_module("github.com/kurtosis-tech/eth2-package/src/prometheus/prometheus_launcher.star")
grafana =import_module("github.com/kurtosis-tech/eth2-package/src/grafana/grafana_launcher.star")
testnet_verifier = import_module("github.com/kurtosis-tech/eth2-package/src/testnet_verifier/testnet_verifier.star")
mev_boost_launcher_module = import_module("github.com/kurtosis-tech/eth2-package/src/mev_boost/mev_boost_launcher.star")
GRAFANA_USER = "admin"
GRAFANA_PASSWORD = "admin"
......@@ -17,6 +18,9 @@ GRAFANA_DASHBOARD_PATH_URL = "/d/QdTOwy-nz/eth2-merge-kurtosis-module-dashboard?
FIRST_NODE_FINALIZATION_FACT = "cl-boot-finalization-fact"
HTTP_PORT_ID_FOR_FACT = "http"
MEV_BOOST_SERVICE_NAME_PREFIX = "mev-boost-"
MEV_BOOST_SHOULD_CHECK_RELAY = True
def run(plan, args):
args_with_right_defaults = parse_input.parse_input(args)
......@@ -30,14 +34,23 @@ def run(plan, args):
plan.print("Read the prometheus, grafana templates")
plan.print("Launching participant network with {0} participants and the following network params {1}".format(num_participants, network_params))
all_participants, cl_gensis_timestamp = participant_network.launch_participant_network(plan, args_with_right_defaults.participants, network_params, args_with_right_defaults.global_client_log_level)
all_participants, cl_genesis_timestamp = eth_network_module.run(plan, args)
all_el_client_contexts = []
all_cl_client_contexts = []
for participant in all_participants:
all_el_client_contexts.append(participant.el_client_context)
all_cl_client_contexts.append(participant.cl_client_context)
# spin up mev boost contexts
all_mevboost_contexts = []
for index, participant in enumerate(args_with_right_defaults.participants):
mev_boost_context = None
if hasattr(participant, "builder_network_params") and participant.builder_network_params != None:
mev_boost_launcher = mev_boost_launcher_module.new_mev_boost_launcher(MEV_BOOST_SHOULD_CHECK_RELAY, participant.builder_network_params.relay_endpoints)
mev_boost_service_name = "{0}{1}".format(MEV_BOOST_SERVICE_NAME_PREFIX, index)
mev_boost_context = mev_boost_launcher_module.launch_mevboost(plan, mev_boost_launcher, mev_boost_service_name, network_params.network_id)
all_mevboost_contexts.append(mev_boost_context)
if not args_with_right_defaults.launch_additional_services:
return
......@@ -51,7 +64,7 @@ def run(plan, args):
plan.print("Launching forkmon")
forkmon_config_template = read_file(static_files.FORKMON_CONFIG_TEMPLATE_FILEPATH)
forkmon.launch_forkmon(plan, forkmon_config_template, all_cl_client_contexts, cl_gensis_timestamp, network_params.seconds_per_slot, network_params.slots_per_epoch)
forkmon.launch_forkmon(plan, forkmon_config_template, all_cl_client_contexts, cl_genesis_timestamp, network_params.seconds_per_slot, network_params.slots_per_epoch)
plan.print("Succesfully launched forkmon")
plan.print("Launching prometheus...")
......
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
mev_boost_context = ("github.com/kurtosis-tech/eth2-package/src/participant_network/mev_boost/mev_boost_context.star")
mev_boost_context = ("github.com/kurtosis-tech/eth2-package/src/mev_boost/mev_boost_context.star")
FLASHBOTS_MEV_BOOST_IMAGE = "flashbots/mev-boost"
FLASHBOTS_MEV_BOOST_PORT = 18550
......
def new_cl_client_context(client_name, enr, ip_addr, http_port_num, cl_nodes_metrics_info, beacon_service_name):
return struct(
client_name = client_name,
enr = enr,
ip_addr = ip_addr,
http_port_num = http_port_num,
cl_nodes_metrics_info = cl_nodes_metrics_info,
beacon_service_name = beacon_service_name
)
def wait_for_healthy(plan, service_name, port_id):
recipe = GetHttpRequestRecipe(
endpoint = "/eth/v1/node/health",
port_id = port_id
)
return plan.wait(recipe = recipe, field = "code", assertion = "IN", target_value = [200, 206, 503], timeout = "15m", service_name = service_name)
# this is a dictionary as this will get serialzed to JSON
def new_cl_node_metrics_info(name, path, url):
return {
"name": name,
"path": path,
"url": url
}
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
cl_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_client_context.star")
cl_node_metrics = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_metrics_info.star")
mev_boost_context_module = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/mev_boost/mev_boost_context.star")
cl_node_health_checker = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_health_checker.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
LIGHTHOUSE_BINARY_COMMAND = "lighthouse"
GENESIS_DATA_MOUNTPOINT_ON_CLIENTS = "/genesis"
VALIDATOR_KEYS_MOUNTPOINT_ON_CLIENTS = "/validator-keys"
RUST_BACKTRACE_ENVVAR_NAME = "RUST_BACKTRACE"
RUST_FULL_BACKTRACE_KEYWORD = "full"
# ---------------------------------- Beacon client -------------------------------------
CONSENSUS_DATA_DIRPATH_ON_BEACON_SERVICE_CONTAINER = "/consensus-data"
# Port IDs
BEACON_TCP_DISCOVERY_PORT_ID = "tcp-discovery"
BEACON_UDP_DISCOVERY_PORT_ID = "udp-discovery"
BEACON_HTTP_PORT_ID = "http"
BEACON_METRICS_PORT_ID = "metrics"
# Port nums
BEACON_DISCOVERY_PORT_NUM = 9000
BEACON_HTTP_PORT_NUM = 4000
BEACON_METRICS_PORT_NUM = 5054
# ---------------------------------- Validator client -------------------------------------
VALIDATING_REWARDS_ACCOUNT = "0x0000000000000000000000000000000000000000"
VALIDATOR_HTTP_PORT_ID = "http"
VALIDATOR_METRICS_PORT_ID = "metrics"
VALIDATOR_HTTP_PORT_NUM = 5042
VALIDATOR_METRICS_PORT_NUM = 5064
VALIDATOR_HTTP_PORT_WAIT_TIMEOUT = "6m"
METRICS_PATH = "/metrics"
BEACON_SUFFIX_SERVICE_NAME = "beacon"
VALIDATOR_SUFFIX_SERVICE_NAME = "validator"
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
BEACON_USED_PORTS = {
BEACON_TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(BEACON_DISCOVERY_PORT_NUM, shared_utils.TCP_PROTOCOL),
BEACON_UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(BEACON_DISCOVERY_PORT_NUM, shared_utils.UDP_PROTOCOL),
BEACON_HTTP_PORT_ID: shared_utils.new_port_spec(BEACON_HTTP_PORT_NUM, shared_utils.TCP_PROTOCOL, shared_utils.HTTP_APPLICATION_PROTOCOL),
BEACON_METRICS_PORT_ID: shared_utils.new_port_spec(BEACON_METRICS_PORT_NUM, shared_utils.TCP_PROTOCOL, shared_utils.HTTP_APPLICATION_PROTOCOL),
}
VALIDATOR_USED_PORTS = {
VALIDATOR_HTTP_PORT_ID: shared_utils.new_port_spec(VALIDATOR_HTTP_PORT_NUM, shared_utils.TCP_PROTOCOL, shared_utils.NOT_PROVIDED_APPLICATION_PROTOCOL, VALIDATOR_HTTP_PORT_WAIT_TIMEOUT),
VALIDATOR_METRICS_PORT_ID: shared_utils.new_port_spec(VALIDATOR_METRICS_PORT_NUM, shared_utils.TCP_PROTOCOL, shared_utils.HTTP_APPLICATION_PROTOCOL),
}
LIGHTHOUSE_LOG_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "error",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "warn",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "info",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "debug",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "trace",
}
def launch(
plan,
launcher,
service_name,
image,
participant_log_level,
global_log_level,
bootnode_context,
el_client_context,
mev_boost_context,
node_keystore_files,
extra_beacon_params,
extra_validator_params):
beacon_node_service_name = "{0}-{1}".format(service_name, BEACON_SUFFIX_SERVICE_NAME)
validator_node_service_name = "{0}-{1}".format(service_name, VALIDATOR_SUFFIX_SERVICE_NAME)
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, LIGHTHOUSE_LOG_LEVELS)
# Launch Beacon node
beacon_config = get_beacon_config(
launcher.genesis_data,
image,
bootnode_context,
el_client_context,
mev_boost_context,
log_level,
extra_beacon_params,
)
beacon_service = plan.add_service(beacon_node_service_name, beacon_config)
cl_node_health_checker.wait_for_healthy(plan, beacon_node_service_name, BEACON_HTTP_PORT_ID)
beacon_http_port = beacon_service.ports[BEACON_HTTP_PORT_ID]
# Launch validator node
beacon_http_url = "http://{0}:{1}".format(beacon_service.ip_address, beacon_http_port.number)
validator_config = get_validator_config(
launcher.genesis_data,
image,
log_level,
beacon_http_url,
node_keystore_files,
mev_boost_context,
extra_validator_params,
)
validator_service = plan.add_service(validator_node_service_name, validator_config)
# TODO(old) add validator availability using the validator API: https://ethereum.github.io/beacon-APIs/?urls.primaryName=v1#/ValidatorRequiredApi | from eth2-merge-kurtosis-module
beacon_node_identity_recipe = GetHttpRequestRecipe(
endpoint = "/eth/v1/node/identity",
port_id = BEACON_HTTP_PORT_ID,
extract = {
"enr": ".data.enr"
}
)
beacon_node_enr = plan.request(recipe = beacon_node_identity_recipe, service_name = beacon_node_service_name)["extract.enr"]
beacon_metrics_port = beacon_service.ports[BEACON_METRICS_PORT_ID]
beacon_metrics_url = "{0}:{1}".format(beacon_service.ip_address, beacon_metrics_port.number)
validator_metrics_port = validator_service.ports[VALIDATOR_METRICS_PORT_ID]
validator_metrics_url = "{0}:{1}".format(validator_service.ip_address, validator_metrics_port.number)
beacon_node_metrics_info = cl_node_metrics.new_cl_node_metrics_info(beacon_node_service_name, METRICS_PATH, beacon_metrics_url)
validator_node_metrics_info = cl_node_metrics.new_cl_node_metrics_info(validator_node_service_name, METRICS_PATH, validator_metrics_url)
nodes_metrics_info = [beacon_node_metrics_info, validator_node_metrics_info]
return cl_client_context.new_cl_client_context(
"lighthouse",
beacon_node_enr,
beacon_service.ip_address,
BEACON_HTTP_PORT_NUM,
nodes_metrics_info,
beacon_node_service_name,
)
def get_beacon_config(
genesis_data,
image,
boot_cl_client_ctx,
el_client_ctx,
mev_boost_context,
log_level,
extra_params):
el_client_engine_rpc_url_str = "http://{0}:{1}".format(
el_client_ctx.ip_addr,
el_client_ctx.engine_rpc_port_num,
)
# For some reason, Lighthouse takes in the parent directory of the config file (rather than the path to the config file itself)
genesis_config_parent_dirpath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNTPOINT_ON_CLIENTS, shared_utils.path_dir(genesis_data.config_yml_rel_filepath))
jwt_secret_filepath = shared_utils.path_join(GENESIS_DATA_MOUNTPOINT_ON_CLIENTS, genesis_data.jwt_secret_rel_filepath)
# NOTE: If connecting to the merge devnet remotely we DON'T want the following flags; when they're not set, the node's external IP address is auto-detected
# from the peers it communicates with but when they're set they basically say "override the autodetection and
# use what I specify instead." This requires having a know external IP address and port, which we definitely won't
# have with a network running in Kurtosis.
# "--disable-enr-auto-update",
# "--enr-address=" + externalIpAddress,
# fmt.Sprintf("--enr-udp-port=%v", BEACON_DISCOVERY_PORT_NUM),
# fmt.Sprintf("--enr-tcp-port=%v", beaconDiscoveryPortNum),
cmd = [
LIGHTHOUSE_BINARY_COMMAND,
"beacon_node",
"--debug-level=" + log_level,
"--datadir=" + CONSENSUS_DATA_DIRPATH_ON_BEACON_SERVICE_CONTAINER,
"--testnet-dir=" + genesis_config_parent_dirpath_on_client,
# vvvvvvvvvvvvvvvvvvv REMOVE THESE WHEN CONNECTING TO EXTERNAL NET vvvvvvvvvvvvvvvvvvvvv
"--disable-enr-auto-update",
"--enr-address=" + PRIVATE_IP_ADDRESS_PLACEHOLDER,
"--enr-udp-port={0}".format(BEACON_DISCOVERY_PORT_NUM),
"--enr-tcp-port={0}".format(BEACON_DISCOVERY_PORT_NUM),
# ^^^^^^^^^^^^^^^^^^^ REMOVE THESE WHEN CONNECTING TO EXTERNAL NET ^^^^^^^^^^^^^^^^^^^^^
"--listen-address=0.0.0.0",
"--port={0}".format(BEACON_DISCOVERY_PORT_NUM), # NOTE: Remove for connecting to external net!
"--http",
"--http-address=0.0.0.0",
"--http-port={0}".format(BEACON_HTTP_PORT_NUM),
"--http-allow-sync-stalled",
# NOTE: This comes from:
# https://github.com/sigp/lighthouse/blob/7c88f582d955537f7ffff9b2c879dcf5bf80ce13/scripts/local_testnet/beacon_node.sh
# and the option says it's "useful for testing in smaller networks" (unclear what happens in larger networks)
"--disable-packet-filter",
"--execution-endpoints=" + el_client_engine_rpc_url_str,
"--jwt-secrets=" + jwt_secret_filepath,
"--suggested-fee-recipient=" + VALIDATING_REWARDS_ACCOUNT,
# Set per Paris' recommendation to reduce noise in the logs
"--subscribe-all-subnets",
# vvvvvvvvvvvvvvvvvvv METRICS CONFIG vvvvvvvvvvvvvvvvvvvvv
"--metrics",
"--metrics-address=0.0.0.0",
"--metrics-allow-origin=*",
"--metrics-port={0}".format(BEACON_METRICS_PORT_NUM),
# ^^^^^^^^^^^^^^^^^^^ METRICS CONFIG ^^^^^^^^^^^^^^^^^^^^^
]
if boot_cl_client_ctx != None:
cmd.append("--boot-nodes="+boot_cl_client_ctx.enr)
if mev_boost_context != None:
cmd.append("--builder")
cmd.append(mev_boost_context_module.mev_boost_endpoint(mev_boost_context))
if len(extra_params) > 0:
# this is a repeated<proto type>, we convert it into Starlark
cmd.extend([param for param in extra_params])
return ServiceConfig(
image = image,
ports = BEACON_USED_PORTS,
cmd = cmd,
files = {
GENESIS_DATA_MOUNTPOINT_ON_CLIENTS: genesis_data.files_artifact_uuid
},
env_vars = {
RUST_BACKTRACE_ENVVAR_NAME: RUST_FULL_BACKTRACE_KEYWORD
},
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def get_validator_config(
genesis_data,
image,
log_level,
beacon_client_http_url,
node_keystore_files,
mev_boost_context,
extra_params):
# For some reason, Lighthouse takes in the parent directory of the config file (rather than the path to the config file itself)
genesis_config_parent_dirpath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNTPOINT_ON_CLIENTS, shared_utils.path_dir(genesis_data.config_yml_rel_filepath))
validator_keys_dirpath = shared_utils.path_join(VALIDATOR_KEYS_MOUNTPOINT_ON_CLIENTS, node_keystore_files.raw_keys_relative_dirpath)
validator_secrets_dirpath = shared_utils.path_join(VALIDATOR_KEYS_MOUNTPOINT_ON_CLIENTS, node_keystore_files.raw_secrets_relative_dirpath)
cmd = [
"lighthouse",
"validator_client",
"--debug-level=" + log_level,
"--testnet-dir=" + genesis_config_parent_dirpath_on_client,
"--validators-dir=" + validator_keys_dirpath,
# NOTE: When secrets-dir is specified, we can't add the --data-dir flag
"--secrets-dir=" + validator_secrets_dirpath,
# The node won't have a slashing protection database and will fail to start otherwise
"--init-slashing-protection",
"--http",
"--unencrypted-http-transport",
"--http-address=0.0.0.0",
"--http-port={0}".format(VALIDATOR_HTTP_PORT_NUM),
"--beacon-nodes=" + beacon_client_http_url,
#"--enable-doppelganger-protection", // Disabled to not have to wait 2 epochs before validator can start
# burn address - If unset, the validator will scream in its logs
"--suggested-fee-recipient=0x0000000000000000000000000000000000000000",
# vvvvvvvvvvvvvvvvvvv PROMETHEUS CONFIG vvvvvvvvvvvvvvvvvvvvv
"--metrics",
"--metrics-address=0.0.0.0",
"--metrics-allow-origin=*",
"--metrics-port={0}".format(VALIDATOR_METRICS_PORT_NUM),
# ^^^^^^^^^^^^^^^^^^^ PROMETHEUS CONFIG ^^^^^^^^^^^^^^^^^^^^^
]
if mev_boost_context != None:
cmd.append("--builder-proposals")
if len(extra_params):
cmd.extend([param for param in extra_params])
return ServiceConfig(
image = image,
ports = VALIDATOR_USED_PORTS,
cmd = cmd,
files = {
GENESIS_DATA_MOUNTPOINT_ON_CLIENTS: genesis_data.files_artifact_uuid,
VALIDATOR_KEYS_MOUNTPOINT_ON_CLIENTS: node_keystore_files.files_artifact_uuid,
},
env_vars = {
RUST_BACKTRACE_ENVVAR_NAME: RUST_FULL_BACKTRACE_KEYWORD
},
)
def new_lighthouse_launcher(cl_genesis_data):
return struct(
genesis_data = cl_genesis_data,
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
cl_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_client_context.star")
cl_node_metrics = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_metrics_info.star")
mev_boost_context_module = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/mev_boost/mev_boost_context.star")
cl_node_health_checker = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_health_checker.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
CONSENSUS_DATA_DIRPATH_ON_SERVICE_CONTAINER = "/consensus-data"
GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER = "/genesis"
VALIDATOR_KEYS_MOUNT_DIRPATH_ON_SERVICE_CONTAINER = "/validator-keys"
# Port IDs
TCP_DISCOVERY_PORT_ID = "tcp-discovery"
UDP_DISCOVERY_PORT_ID = "udp-discovery"
HTTP_PORT_ID = "http"
METRICS_PORT_ID = "metrics"
VALIDATOR_METRICS_PORT_ID = "validator-metrics"
# Port nums
DISCOVERY_PORT_NUM = 9000
HTTP_PORT_NUM = 4000
METRICS_PORT_NUM = 8008
VALIDATOR_METRICS_PORT_NUM = 5064
BEACON_SUFFIX_SERVICE_NAME = "beacon"
VALIDATOR_SUFFIX_SERVICE_NAME = "validator"
METRICS_PATH = "/metrics"
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
USED_PORTS = {
TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.TCP_PROTOCOL),
UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.UDP_PROTOCOL),
HTTP_PORT_ID: shared_utils.new_port_spec(HTTP_PORT_NUM, shared_utils.TCP_PROTOCOL),
METRICS_PORT_ID: shared_utils.new_port_spec(METRICS_PORT_NUM, shared_utils.TCP_PROTOCOL),
VALIDATOR_METRICS_PORT_ID: shared_utils.new_port_spec(VALIDATOR_METRICS_PORT_NUM, shared_utils.TCP_PROTOCOL)
}
LODESTAR_LOG_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "error",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "warn",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "info",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "debug",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "trace",
}
def launch(
plan,
launcher,
service_name,
image,
participant_log_level,
global_log_level,
bootnode_context,
el_client_context,
mev_boost_context,
node_keystore_files,
extra_beacon_params,
extra_validator_params):
beacon_node_service_name = "{0}-{1}".format(service_name, BEACON_SUFFIX_SERVICE_NAME)
validator_node_service_name = "{0}-{1}".format(service_name, VALIDATOR_SUFFIX_SERVICE_NAME)
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, LODESTAR_LOG_LEVELS)
# Launch Beacon node
beacon_config = get_beacon_config(
launcher.cl_genesis_data,
image,
bootnode_context,
el_client_context,
mev_boost_context,
log_level,
extra_beacon_params,
)
beacon_service = plan.add_service(beacon_node_service_name, beacon_config)
beacon_http_port = beacon_service.ports[HTTP_PORT_ID]
cl_node_health_checker.wait_for_healthy(plan, beacon_node_service_name, HTTP_PORT_ID)
# Launch validator node
beacon_http_url = "http://{0}:{1}".format(beacon_service.ip_address, beacon_http_port.number)
validator_config = get_validator_config(
validator_node_service_name,
launcher.cl_genesis_data,
image,
log_level,
beacon_http_url,
node_keystore_files,
mev_boost_context,
extra_validator_params,
)
validator_service = plan.add_service(validator_node_service_name, validator_config)
# TODO(old) add validator availability using the validator API: https://ethereum.github.io/beacon-APIs/?urls.primaryName=v1#/ValidatorRequiredApi | from eth2-merge-kurtosis-module
beacon_node_identity_recipe = GetHttpRequestRecipe(
endpoint = "/eth/v1/node/identity",
port_id = HTTP_PORT_ID,
extract = {
"enr": ".data.enr"
}
)
beacon_node_enr = plan.request(recipe = beacon_node_identity_recipe, service_name = beacon_node_service_name)["extract.enr"]
beacon_metrics_port = beacon_service.ports[METRICS_PORT_ID]
beacon_metrics_url = "{0}:{1}".format(beacon_service.ip_address, beacon_metrics_port.number)
beacon_node_metrics_info = cl_node_metrics.new_cl_node_metrics_info(service_name, METRICS_PATH, beacon_metrics_url)
nodes_metrics_info = [beacon_node_metrics_info]
return cl_client_context.new_cl_client_context(
"lodestar",
beacon_node_enr,
beacon_service.ip_address,
HTTP_PORT_NUM,
nodes_metrics_info,
beacon_node_service_name
)
def get_beacon_config(
genesis_data,
image,
boot_cl_client_ctx,
el_client_ctx,
mev_boost_context,
log_level,
extra_params):
el_client_rpc_url_str = "http://{0}:{1}".format(
el_client_ctx.ip_addr,
el_client_ctx.rpc_port_num,
)
el_client_engine_rpc_url_str = "http://{0}:{1}".format(
el_client_ctx.ip_addr,
el_client_ctx.engine_rpc_port_num,
)
genesis_config_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.config_yml_rel_filepath)
genesis_ssz_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.genesis_ssz_rel_filepath)
jwt_secret_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.jwt_secret_rel_filepath)
cmd = [
"beacon",
"--logLevel=" + log_level,
"--port={0}".format(DISCOVERY_PORT_NUM),
"--discoveryPort={0}".format(DISCOVERY_PORT_NUM),
"--dataDir=" + CONSENSUS_DATA_DIRPATH_ON_SERVICE_CONTAINER,
"--paramsFile=" + genesis_config_filepath,
"--genesisStateFile=" + genesis_ssz_filepath,
"--eth1.depositContractDeployBlock=0",
"--network.connectToDiscv5Bootnodes=true",
"--discv5=true",
"--eth1=true",
"--eth1.providerUrls=" + el_client_rpc_url_str,
"--execution.urls=" + el_client_engine_rpc_url_str,
"--rest=true",
"--rest.address=0.0.0.0",
"--rest.namespace=*",
"--rest.port={0}".format(HTTP_PORT_NUM),
"--enr.ip=" + PRIVATE_IP_ADDRESS_PLACEHOLDER,
"--enr.tcp={0}".format(DISCOVERY_PORT_NUM),
"--enr.udp={0}".format(DISCOVERY_PORT_NUM),
# Set per Pari's recommendation to reduce noise in the logs
"--subscribeAllSubnets=true",
"--jwt-secret={0}".format(jwt_secret_filepath),
# vvvvvvvvvvvvvvvvvvv METRICS CONFIG vvvvvvvvvvvvvvvvvvvvv
"--metrics",
"--metrics.address=0.0.0.0",
"--metrics.port={0}".format(METRICS_PORT_NUM),
# ^^^^^^^^^^^^^^^^^^^ METRICS CONFIG ^^^^^^^^^^^^^^^^^^^^^
]
if boot_cl_client_ctx != None :
cmd.append("--bootnodes="+boot_cl_client_ctx.enr)
if mev_boost_context != None:
cmd.append("--builder")
cmd.append("--builder.urls '{0}'".format(mev_boost_context_module.mev_boost_endpoint(mev_boost_context)))
if len(extra_params) > 0:
# this is a repeated<proto type>, we convert it into Starlark
cmd.extend([param for param in extra_params])
return ServiceConfig(
image = image,
ports = USED_PORTS,
cmd = cmd,
files = {
GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER: genesis_data.files_artifact_uuid
},
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def get_validator_config(
service_name,
genesis_data,
image,
log_level,
beacon_client_http_url,
node_keystore_files,
mev_boost_context,
extra_params):
root_dirpath = shared_utils.path_join(CONSENSUS_DATA_DIRPATH_ON_SERVICE_CONTAINER, service_name)
genesis_config_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.config_yml_rel_filepath)
validator_keys_dirpath = shared_utils.path_join(VALIDATOR_KEYS_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, node_keystore_files.raw_keys_relative_dirpath)
validator_secrets_dirpath = shared_utils.path_join(VALIDATOR_KEYS_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, node_keystore_files.raw_secrets_relative_dirpath)
cmd = [
"validator",
"--logLevel=" + log_level,
"--dataDir=" + root_dirpath,
"--paramsFile=" + genesis_config_filepath,
"--server=" + beacon_client_http_url,
"--keystoresDir=" + validator_keys_dirpath,
"--secretsDir=" + validator_secrets_dirpath,
# vvvvvvvvvvvvvvvvvvv PROMETHEUS CONFIG vvvvvvvvvvvvvvvvvvvvv
"--metrics",
"--metrics.address=0.0.0.0",
"--metrics.port={0}".format(VALIDATOR_METRICS_PORT_NUM),
# ^^^^^^^^^^^^^^^^^^^ PROMETHEUS CONFIG ^^^^^^^^^^^^^^^^^^^^^
]
if mev_boost_context != None:
cmd.append("--builder")
# TODO(old) required to work? - from old module
# cmdArgs = append(cmdArgs, "--defaultFeeRecipient <your ethereum address>")
if len(extra_params) > 0:
# this is a repeated<proto type>, we convert it into Starlark
cmd.extend([param for param in extra_params])
return ServiceConfig(
image = image,
ports = USED_PORTS,
cmd = cmd,
files = {
GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER: genesis_data.files_artifact_uuid,
VALIDATOR_KEYS_MOUNT_DIRPATH_ON_SERVICE_CONTAINER: node_keystore_files.files_artifact_uuid,
},
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def new_lodestar_launcher(cl_genesis_data):
return struct(
cl_genesis_data = cl_genesis_data,
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
cl_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_client_context.star")
cl_node_metrics = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_metrics_info.star")
cl_node_health_checker = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_health_checker.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
GENESIS_DATA_MOUNTPOINT_ON_CLIENT = "/genesis-data"
VALIDATOR_KEYS_MOUNTPOINT_ON_CLIENT = "/validator-keys"
# Port IDs
TCP_DISCOVERY_PORT_ID = "tcp-discovery"
UDP_DISCOVERY_PORT_ID = "udp-discovery"
HTTP_PORT_ID = "http"
METRICS_PORT_ID = "metrics"
# Port nums
DISCOVERY_PORT_NUM = 9000
HTTP_PORT_NUM = 4000
METRICS_PORT_NUM = 8008
# Nimbus requires that its data directory already exists (because it expects you to bind-mount it), so we
# have to to create it
CONSENSUS_DATA_DIRPATH_IN_SERVICE_CONTAINER = "$HOME/consensus-data"
# Nimbus wants the data dir to have these perms
CONSENSUS_DATA_DIR_PERMS_STR = "0700"
# The entrypoint the image normally starts with (we need to override the entrypoint to create the
# consensus data directory on the image before it starts)
DEFAULT_IMAGE_ENTRYPOINT = "/home/user/nimbus-eth2/build/nimbus_beacon_node"
# Nimbus needs write access to the validator keys/secrets directories, and b/c the module container runs as root
# while the Nimbus container does not, we can't just point the Nimbus binary to the paths in the shared dir because
# it won't be able to open them. To get around this, we copy the validator keys/secrets to a path inside the Nimbus
# container that is owned by the container's user
VALIDATOR_KEYS_DIRPATH_ON_SERVICE_CONTAINER = "$HOME/validator-keys"
VALIDATOR_SECRETS_DIRPATH_ON_SERVICE_CONTAINER = "$HOME/validator-secrets"
METRICS_PATH = "/metrics"
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
USED_PORTS = {
TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.TCP_PROTOCOL),
UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.UDP_PROTOCOL),
HTTP_PORT_ID: shared_utils.new_port_spec(HTTP_PORT_NUM, shared_utils.TCP_PROTOCOL),
METRICS_PORT_ID: shared_utils.new_port_spec(METRICS_PORT_NUM, shared_utils.TCP_PROTOCOL),
}
NIMBUS_LOG_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "ERROR",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "WARN",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "INFO",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "DEBUG",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "TRACE",
}
ENTRYPOINT_ARGS = ["sh", "-c"]
def launch(
plan,
launcher,
service_name,
image,
participant_log_level,
global_log_level,
bootnode_context,
el_client_context,
mev_boost_context,
node_keystore_files,
extra_beacon_params,
extra_validator_params):
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, NIMBUS_LOG_LEVELS)
extra_params = [param for param in extra_beacon_params] + [param for param in extra_validator_params]
config = get_config(launcher.cl_genesis_data, image, bootnode_context, el_client_context, mev_boost_context, log_level, node_keystore_files, extra_params)
nimbus_service = plan.add_service(service_name, config)
cl_node_health_checker.wait_for_healthy(plan, service_name, HTTP_PORT_ID)
cl_node_identity_recipe = GetHttpRequestRecipe(
endpoint = "/eth/v1/node/identity",
port_id = HTTP_PORT_ID,
extract = {
"enr": ".data.enr"
}
)
node_enr = plan.request(recipe = cl_node_identity_recipe, service_name = service_name)["extract.enr"]
metrics_port = nimbus_service.ports[METRICS_PORT_ID]
metrics_url = "{0}:{1}".format(nimbus_service.ip_address, metrics_port.number)
nimbus_node_metrics_info = cl_node_metrics.new_cl_node_metrics_info(service_name, METRICS_PATH, metrics_url)
nodes_metrics_info = [nimbus_node_metrics_info]
return cl_client_context.new_cl_client_context(
"nimbus",
node_enr,
nimbus_service.ip_address,
HTTP_PORT_NUM,
nodes_metrics_info,
service_name,
)
def get_config(
genesis_data,
image,
boot_cl_client_ctx,
el_client_ctx,
mev_boost_context,
log_level,
node_keystore_files,
extra_params):
el_client_engine_rpc_url_str = "http://{0}:{1}".format(
el_client_ctx.ip_addr,
el_client_ctx.engine_rpc_port_num,
)
# For some reason, Nimbus takes in the parent directory of the config file (rather than the path to the config file itself)
genesis_config_parent_dirpath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNTPOINT_ON_CLIENT, shared_utils.path_dir(genesis_data.config_yml_rel_filepath))
jwt_secret_filepath = shared_utils.path_join(GENESIS_DATA_MOUNTPOINT_ON_CLIENT, genesis_data.jwt_secret_rel_filepath)
validator_keys_dirpath = shared_utils.path_join(VALIDATOR_KEYS_MOUNTPOINT_ON_CLIENT, node_keystore_files.nimbus_keys_relative_dirpath)
validator_secrets_dirpath = shared_utils.path_join(VALIDATOR_KEYS_MOUNTPOINT_ON_CLIENT, node_keystore_files.raw_secrets_relative_dirpath)
# Sources for these flags:
# 1) https://github.com/status-im/nimbus-eth2/blob/stable/scripts/launch_local_testnet.sh
# 2) https://github.com/status-im/nimbus-eth2/blob/67ab477a27e358d605e99bffeb67f98d18218eca/scripts/launch_local_testnet.sh#L417
# WARNING: Do NOT set the --max-peers flag here, as doing so to the exact number of nodes seems to mess things up!
# See: https://github.com/kurtosis-tech/eth2-merge-kurtosis-module/issues/26
cmd = [
"mkdir",
CONSENSUS_DATA_DIRPATH_IN_SERVICE_CONTAINER,
"-m",
CONSENSUS_DATA_DIR_PERMS_STR,
"&&",
# TODO(old) COMMENT THIS OUT?
"cp",
"-R",
validator_keys_dirpath,
VALIDATOR_KEYS_DIRPATH_ON_SERVICE_CONTAINER,
"&&",
"cp",
"-R",
validator_secrets_dirpath,
VALIDATOR_SECRETS_DIRPATH_ON_SERVICE_CONTAINER,
"&&",
# If we don't do this chmod, Nimbus will spend a crazy amount of time manually correcting them
# before it starts
"chmod",
"600",
VALIDATOR_SECRETS_DIRPATH_ON_SERVICE_CONTAINER + "/*",
"&&",
DEFAULT_IMAGE_ENTRYPOINT,
"--non-interactive=true",
"--log-level=" + log_level,
"--network=" + genesis_config_parent_dirpath_on_client,
"--data-dir=" + CONSENSUS_DATA_DIRPATH_IN_SERVICE_CONTAINER,
"--web3-url=" + el_client_engine_rpc_url_str,
"--nat=extip:" + PRIVATE_IP_ADDRESS_PLACEHOLDER,
"--enr-auto-update=false",
"--rest",
"--rest-address=0.0.0.0",
"--rest-port={0}".format(HTTP_PORT_NUM),
"--validators-dir=" + VALIDATOR_KEYS_DIRPATH_ON_SERVICE_CONTAINER,
"--secrets-dir=" + VALIDATOR_SECRETS_DIRPATH_ON_SERVICE_CONTAINER,
# There's a bug where if we don't set this flag, the Nimbus nodes won't work:
# https://discord.com/channels/641364059387854899/674288681737256970/922890280120750170
# https://github.com/status-im/nimbus-eth2/issues/2451
"--doppelganger-detection=false",
# Set per Pari's recommendation to reduce noise in the logs
"--subscribe-all-subnets=true",
# Nimbus can handle a max of 256 threads, if the host has more then nimbus crashes. Setting it to 4 so it doesn't crash on build servers
"--num-threads=4",
"--jwt-secret={0}".format(jwt_secret_filepath),
# vvvvvvvvvvvvvvvvvvv METRICS CONFIG vvvvvvvvvvvvvvvvvvvvv
"--metrics",
"--metrics-address=0.0.0.0",
"--metrics-port={0}".format(METRICS_PORT_NUM),
# ^^^^^^^^^^^^^^^^^^^ METRICS CONFIG ^^^^^^^^^^^^^^^^^^^^^
]
if boot_cl_client_ctx == None:
# Copied from https://.com/status-im/nimbus-eth2/blob/67ab477a27e358d605e99bffeb67f98d18218eca/scripts/launch_local_testnet.sh#L417
# See explanation there
cmd.append("--subscribe-all-subnets")
else:
cmd.append("--bootstrap-node="+boot_cl_client_ctx.enr)
if mev_boost_context != None:
# TODO(old) add `mev-boost` support once the feature lands on `stable` - from eth2-merge-kurtosis-module
pass
if len(extra_params) > 0:
cmd.extend([param for param in extra_params])
cmd_str = " ".join(cmd)
return ServiceConfig(
image = image,
ports = USED_PORTS,
cmd = [cmd_str],
entrypoint = ENTRYPOINT_ARGS,
files = {
GENESIS_DATA_MOUNTPOINT_ON_CLIENT: genesis_data.files_artifact_uuid,
VALIDATOR_KEYS_MOUNTPOINT_ON_CLIENT: node_keystore_files.files_artifact_uuid
},
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def new_nimbus_launcher(cl_genesis_data):
return struct(
cl_genesis_data = cl_genesis_data,
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
cl_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_client_context.star")
cl_node_metrics = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_metrics_info.star")
cl_node_health_checker = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_health_checker.star")
mev_boost_context_module = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/mev_boost/mev_boost_context.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
IMAGE_SEPARATOR_DELIMITER = ","
EXPECTED_NUM_IMAGES = 2
CONSENSUS_DATA_DIRPATH_ON_SERVICE_CONTAINER = "/consensus-data"
GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER = "/genesis"
VALIDATOR_KEYS_MOUNT_DIRPATH_ON_SERVICE_CONTAINER = "/validator-keys"
PRYSM_PASSWORD_MOUNT_DIRPATH_ON_SERVICE_CONTAINER = "/prysm-password"
# Port IDs
TCP_DISCOVERY_PORT_ID = "tcp-discovery"
UDP_DISCOVERY_PORT_ID = "udp-discovery"
RPC_PORT_ID = "rpc"
HTTP_PORT_ID = "http"
BEACON_MONITORING_PORT_ID = "monitoring"
VALIDATOR_MONITORING_PORT_ID = "monitoring"
# Port nums
DISCOVERY_TCP_PORT_NUM = 13000
DISCOVERY_UDP_PORT_NUM = 12000
RPC_PORT_NUM = 4000
HTTP_PORT_NUM = 3500
BEACON_MONITORING_PORT_NUM = 8080
VALIDATOR_MONITORING_PORT_NUM = 8081
BEACON_SUFFIX_SERVICE_NAME = "beacon"
VALIDATOR_SUFFIX_SERVICE_NAME = "validator"
MIN_PEERS = 1
METRICS_PATH = "/metrics"
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
BEACON_NODE_USED_PORTS = {
TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_TCP_PORT_NUM, shared_utils.TCP_PROTOCOL),
UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_UDP_PORT_NUM, shared_utils.UDP_PROTOCOL),
RPC_PORT_ID: shared_utils.new_port_spec(RPC_PORT_NUM, shared_utils.TCP_PROTOCOL),
HTTP_PORT_ID: shared_utils.new_port_spec(HTTP_PORT_NUM, shared_utils.TCP_PROTOCOL),
BEACON_MONITORING_PORT_ID: shared_utils.new_port_spec(BEACON_MONITORING_PORT_NUM, shared_utils.TCP_PROTOCOL),
}
VALIDATOR_NODE_USED_PORTS = {
VALIDATOR_MONITORING_PORT_ID: shared_utils.new_port_spec(VALIDATOR_MONITORING_PORT_NUM, shared_utils.TCP_PROTOCOL),
}
PRYSM_LOG_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "error",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "warn",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "info",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "debug",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "trace",
}
def launch(
plan,
launcher,
service_name,
images,
participant_log_level,
global_log_level,
bootnode_context,
el_client_context,
mev_boost_context,
node_keystore_files,
extra_beacon_params,
extra_validator_params):
split_images = images.split(IMAGE_SEPARATOR_DELIMITER)
if len(split_images) != EXPECTED_NUM_IMAGES:
fail("Expected {0} images but got {1}".format(EXPECTED_NUM_IMAGES, len(split_images)))
beacon_image, validator_image = split_images
if beacon_image.strip() == "":
fail("An empty beacon image was provided")
if validator_image.strip() == "":
fail("An empty validator image was provided")
beacon_node_service_name = "{0}-{1}".format(service_name, BEACON_SUFFIX_SERVICE_NAME)
validator_node_service_name = "{0}-{1}".format(service_name, VALIDATOR_SUFFIX_SERVICE_NAME)
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, PRYSM_LOG_LEVELS)
beacon_config = get_beacon_config(
launcher.genesis_data,
beacon_image,
bootnode_context,
el_client_context,
mev_boost_context,
log_level,
extra_beacon_params,
)
beacon_service = plan.add_service(beacon_node_service_name, beacon_config)
cl_node_health_checker.wait_for_healthy(plan, beacon_node_service_name, HTTP_PORT_ID)
beacon_http_port = beacon_service.ports[HTTP_PORT_ID]
# Launch validator node
beacon_http_endpoint = "{0}:{1}".format(beacon_service.ip_address, HTTP_PORT_NUM)
beacon_rpc_endpoint = "{0}:{1}".format(beacon_service.ip_address, RPC_PORT_NUM)
validator_config = get_validator_config(
launcher.genesis_data,
validator_image,
validator_node_service_name,
log_level,
beacon_rpc_endpoint,
beacon_http_endpoint,
node_keystore_files,
mev_boost_context,
extra_validator_params,
launcher.prysm_password_relative_filepath,
launcher.prysm_password_artifact_uuid
)
validator_service = plan.add_service(validator_node_service_name, validator_config)
# TODO(old) add validator availability using the validator API: https://ethereum.github.io/beacon-APIs/?urls.primaryName=v1#/ValidatorRequiredApi | from eth2-merge-kurtosis-module
beacon_node_identity_recipe = GetHttpRequestRecipe(
endpoint = "/eth/v1/node/identity",
port_id = HTTP_PORT_ID,
extract = {
"enr": ".data.enr"
}
)
beacon_node_enr = plan.request(recipe = beacon_node_identity_recipe, service_name = beacon_node_service_name)["extract.enr"]
beacon_metrics_port = beacon_service.ports[BEACON_MONITORING_PORT_ID]
beacon_metrics_url = "{0}:{1}".format(beacon_service.ip_address, beacon_metrics_port.number)
validator_metrics_port = validator_service.ports[VALIDATOR_MONITORING_PORT_ID]
validator_metrics_url = "{0}:{1}".format(validator_service.ip_address, validator_metrics_port.number)
beacon_node_metrics_info = cl_node_metrics.new_cl_node_metrics_info(beacon_node_service_name, METRICS_PATH, beacon_metrics_url)
validator_node_metrics_info = cl_node_metrics.new_cl_node_metrics_info(validator_node_service_name, METRICS_PATH, validator_metrics_url)
nodes_metrics_info = [beacon_node_metrics_info, validator_node_metrics_info]
return cl_client_context.new_cl_client_context(
"prysm",
beacon_node_enr,
beacon_service.ip_address,
HTTP_PORT_NUM,
nodes_metrics_info,
beacon_node_service_name
)
def get_beacon_config(
genesis_data,
beacon_image,
bootnode_context,
el_client_context,
mev_boost_context,
log_level,
extra_params,
):
el_client_engine_rpc_url_str = "http://{0}:{1}".format(
el_client_context.ip_addr,
el_client_context.engine_rpc_port_num,
)
genesis_config_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.config_yml_rel_filepath)
genesis_ssz_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.genesis_ssz_rel_filepath)
jwt_secret_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.jwt_secret_rel_filepath)
cmd = [
"--accept-terms-of-use=true", #it's mandatory in order to run the node
"--datadir=" + CONSENSUS_DATA_DIRPATH_ON_SERVICE_CONTAINER,
"--chain-config-file=" + genesis_config_filepath,
"--genesis-state=" + genesis_ssz_filepath,
"--http-web3provider=" + el_client_engine_rpc_url_str,
"--rpc-host=" + PRIVATE_IP_ADDRESS_PLACEHOLDER,
"--rpc-port={0}".format(RPC_PORT_NUM),
"--grpc-gateway-host=0.0.0.0",
"--grpc-gateway-port={0}".format(HTTP_PORT_NUM),
"--p2p-tcp-port={0}".format(DISCOVERY_TCP_PORT_NUM),
"--p2p-udp-port={0}".format(DISCOVERY_UDP_PORT_NUM),
"--min-sync-peers={0}".format(MIN_PEERS),
"--verbosity=" + log_level,
# Set per Pari's recommendation to reduce noise
"--subscribe-all-subnets=true",
"--jwt-secret={0}".format(jwt_secret_filepath),
# vvvvvvvvv METRICS CONFIG vvvvvvvvvvvvvvvvvvvvv
"--disable-monitoring=false",
"--monitoring-host=0.0.0.0",
"--monitoring-port={0}".format(BEACON_MONITORING_PORT_NUM)
# ^^^^^^^^^^^^^^^^^^^ METRICS CONFIG ^^^^^^^^^^^^^^^^^^^^^
]
if bootnode_context != None:
cmd.append("--bootstrap-node="+bootnode_context.enr)
if mev_boost_context != None:
cmd.append(("--http-mev-relay{0}".format(mev_boost_context_module.mev_boost_endpoint(mev_boost_context))))
if len(extra_params) > 0:
# we do the for loop as otherwise its a proto repeated array
cmd.extend([param for param in extra_params])
return ServiceConfig(
image = beacon_image,
ports = BEACON_NODE_USED_PORTS,
cmd = cmd,
files = {
GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER: genesis_data.files_artifact_uuid,
},
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def get_validator_config(
genesis_data,
validator_image,
service_name,
log_level,
beacon_rpc_endpoint,
beacon_http_endpoint,
node_keystore_files,
mev_boost_context,
extra_params,
prysm_password_relative_filepath,
prysm_password_artifact_uuid
):
consensus_data_dirpath = shared_utils.path_join(CONSENSUS_DATA_DIRPATH_ON_SERVICE_CONTAINER, service_name)
prysm_keystore_dirpath = shared_utils.path_join(VALIDATOR_KEYS_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, node_keystore_files.prysm_relative_dirpath)
prysm_password_filepath = shared_utils.path_join(PRYSM_PASSWORD_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, prysm_password_relative_filepath)
cmd = [
"--accept-terms-of-use=true",#it's mandatory in order to run the node
"--prater", #it's a tesnet setup, it's mandatory to set a network (https://docs.prylabs.network/docs/install/install-with-script#before-you-begin-pick-your-network-1)
"--beacon-rpc-gateway-provider=" + beacon_http_endpoint,
"--beacon-rpc-provider=" + beacon_rpc_endpoint,
"--wallet-dir=" + prysm_keystore_dirpath,
"--wallet-password-file=" + prysm_password_filepath,
"--datadir=" + consensus_data_dirpath,
"--monitoring-port={0}".format(VALIDATOR_MONITORING_PORT_NUM),
"--verbosity=" + log_level,
# TODO(old) SOMETHING ABOUT JWT
# vvvvvvvvvvvvvvvvvvv METRICS CONFIG vvvvvvvvvvvvvvvvvvvvv
"--disable-monitoring=false",
"--monitoring-host=0.0.0.0",
"--monitoring-port={0}".format(VALIDATOR_MONITORING_PORT_NUM)
# ^^^^^^^^^^^^^^^^^^^ METRICS CONFIG ^^^^^^^^^^^^^^^^^^^^^
]
if mev_boost_context != None:
# TODO(old) required to work?
# cmdArgs = append(cmdArgs, "--suggested-fee-recipient=0x...")
cmd.append("--enable-builder")
if len(extra_params) > 0:
# we do the for loop as otherwise its a proto repeated array
cmd.extend([param for param in extra_params])
return ServiceConfig(
image = validator_image,
ports = VALIDATOR_NODE_USED_PORTS,
cmd = cmd,
files = {
GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER: genesis_data.files_artifact_uuid,
VALIDATOR_KEYS_MOUNT_DIRPATH_ON_SERVICE_CONTAINER: node_keystore_files.files_artifact_uuid,
PRYSM_PASSWORD_MOUNT_DIRPATH_ON_SERVICE_CONTAINER: prysm_password_artifact_uuid,
},
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def new_prysm_launcher(genesis_data, prysm_password_relative_filepath, prysm_password_artifact_uuid):
return struct(
genesis_data = genesis_data,
prysm_password_artifact_uuid = prysm_password_artifact_uuid,
prysm_password_relative_filepath = prysm_password_relative_filepath
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
cl_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_client_context.star")
cl_node_metrics = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_metrics_info.star")
cl_node_health_checker = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/cl_node_health_checker.star")
mev_boost_context_module = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/mev_boost/mev_boost_context.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
TEKU_BINARY_FILEPATH_IN_IMAGE = "/opt/teku/bin/teku"
GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER = "/genesis"
# The Docker container runs as the "teku" user so we can't write to root
CONSENSUS_DATA_DIRPATH_ON_SERVICE_CONTAINER = "/opt/teku/consensus-data"
# These will get mounted as root and Teku needs directory write permissions, so we'll copy this
# into the Teku user's home directory to get around it
VALIDATOR_KEYS_DIRPATH_ON_SERVICE_CONTAINER = "/validator-keys"
# TODO(old) Get rid of this being hardcoded; should be shared
VALIDATING_REWARDS_ACCOUNT = "0x0000000000000000000000000000000000000000"
# Port IDs
TCP_DISCOVERY_PORT_ID = "tcp-discovery"
UDP_DISCOVERY_PORT_ID = "udp-discovery"
HTTP_PORT_ID = "http"
METRICS_PORT_ID = "metrics"
# Port nums
DISCOVERY_PORT_NUM = 9000
HTTP_PORT_NUM = 4000
METRICS_PORT_NUM = 8008
# 1) The Teku container runs as the "teku" user
# 2) Teku requires write access to the validator secrets directory, so it can write a lockfile into it as it uses the keys
# 3) The module container runs as 'root'
# With these three things combined, it means that when the module container tries to write the validator keys/secrets into
# the shared directory, it does so as 'root'. When Teku tries to consum the same files, it will get a failure because it
# doesn't have permission to write to the 'validator-secrets' directory.
# To get around this, we copy the files AGAIN from
DEST_VALIDATOR_KEYS_DIRPATH_IN_SERVICE_CONTAINER = "$HOME/validator-keys"
DEST_VALIDATOR_SECRETS_DIRPATH_IN_SERVICE_CONTAINER = "$HOME/validator-secrets"
MIN_PEERS = 1
METRICS_PATH = "/metrics"
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
USED_PORTS = {
TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.TCP_PROTOCOL),
UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.UDP_PROTOCOL),
HTTP_PORT_ID: shared_utils.new_port_spec(HTTP_PORT_NUM, shared_utils.TCP_PROTOCOL),
METRICS_PORT_ID: shared_utils.new_port_spec(METRICS_PORT_NUM, shared_utils.TCP_PROTOCOL),
}
ENTRYPOINT_ARGS = ["sh", "-c"]
TEKU_LOG_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "ERROR",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "WARN",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "INFO",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "DEBUG",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "TRACE",
}
def launch(
plan,
launcher,
service_name,
image,
participant_log_level,
global_log_level,
bootnode_context,
el_client_context,
mev_boost_context,
node_keystore_files,
extra_beacon_params,
extra_validator_params):
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, TEKU_LOG_LEVELS)
extra_params = [param for param in extra_beacon_params] + [param for param in extra_validator_params]
config = get_config(launcher.cl_genesis_data, image, bootnode_context, el_client_context, mev_boost_context, log_level, node_keystore_files, extra_params)
teku_service = plan.add_service(service_name, config)
cl_node_health_checker.wait_for_healthy(plan, service_name, HTTP_PORT_ID)
node_identity_recipe = GetHttpRequestRecipe(
endpoint = "/eth/v1/node/identity",
port_id = HTTP_PORT_ID,
extract = {
"enr": ".data.enr"
}
)
node_enr = plan.request(recipe = node_identity_recipe, service_name = service_name)["extract.enr"]
teku_metrics_port = teku_service.ports[METRICS_PORT_ID]
teku_metrics_url = "{0}:{1}".format(teku_service.ip_address, teku_metrics_port.number)
teku_node_metrics_info = cl_node_metrics.new_cl_node_metrics_info(service_name, METRICS_PATH, teku_metrics_url)
nodes_metrics_info = [teku_node_metrics_info]
return cl_client_context.new_cl_client_context(
"teku",
node_enr,
teku_service.ip_address,
HTTP_PORT_NUM,
nodes_metrics_info,
service_name
)
def get_config(
genesis_data,
image,
boot_cl_client_ctx,
el_client_ctx,
mev_boost_context,
log_level,
node_keystore_files,
extra_params):
el_client_rpc_url_str = "http://{0}:{1}".format(
el_client_ctx.ip_addr,
el_client_ctx.rpc_port_num,
)
el_client_engine_rpc_url_str = "http://{0}:{1}".format(
el_client_ctx.ip_addr,
el_client_ctx.engine_rpc_port_num,
)
genesis_config_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.config_yml_rel_filepath)
genesis_ssz_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.genesis_ssz_rel_filepath)
jwt_secret_filepath = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER, genesis_data.jwt_secret_rel_filepath)
validator_keys_dirpath = shared_utils.path_join(VALIDATOR_KEYS_DIRPATH_ON_SERVICE_CONTAINER, node_keystore_files.teku_keys_relative_dirpath)
validator_secrets_dirpath = shared_utils.path_join(VALIDATOR_KEYS_DIRPATH_ON_SERVICE_CONTAINER, node_keystore_files.teku_secrets_relative_dirpath)
cmd = [
# Needed because the generated keys are owned by root and the Teku image runs as the 'teku' user
"cp",
"-R",
validator_keys_dirpath,
DEST_VALIDATOR_KEYS_DIRPATH_IN_SERVICE_CONTAINER,
"&&",
# Needed because the generated keys are owned by root and the Teku image runs as the 'teku' user
"cp",
"-R",
validator_secrets_dirpath,
DEST_VALIDATOR_SECRETS_DIRPATH_IN_SERVICE_CONTAINER,
"&&",
TEKU_BINARY_FILEPATH_IN_IMAGE,
"--Xee-version kilnv2",
"--logging=" + log_level,
"--log-destination=CONSOLE",
"--network=" + genesis_config_filepath,
"--initial-state=" + genesis_ssz_filepath,
"--data-path=" + CONSENSUS_DATA_DIRPATH_ON_SERVICE_CONTAINER,
"--data-storage-mode=PRUNE",
"--p2p-enabled=true",
# Set per Pari's recommendation, to reduce noise in the logs
"--p2p-subscribe-all-subnets-enabled=true",
"--p2p-peer-lower-bound={0}".format(MIN_PEERS),
"--eth1-endpoints=" + el_client_rpc_url_str,
"--p2p-advertised-ip=" + PRIVATE_IP_ADDRESS_PLACEHOLDER,
"--rest-api-enabled=true",
"--rest-api-docs-enabled=true",
"--rest-api-interface=0.0.0.0",
"--rest-api-port={0}".format(HTTP_PORT_NUM),
"--rest-api-host-allowlist=*",
"--data-storage-non-canonical-blocks-enabled=true",
"--validator-keys={0}:{1}".format(
DEST_VALIDATOR_KEYS_DIRPATH_IN_SERVICE_CONTAINER,
DEST_VALIDATOR_SECRETS_DIRPATH_IN_SERVICE_CONTAINER,
),
"--ee-jwt-secret-file={0}".format(jwt_secret_filepath),
"--ee-endpoint=" + el_client_engine_rpc_url_str,
"--validators-proposer-default-fee-recipient=" + VALIDATING_REWARDS_ACCOUNT,
# vvvvvvvvvvvvvvvvvvv METRICS CONFIG vvvvvvvvvvvvvvvvvvvvv
"--metrics-enabled",
"--metrics-interface=0.0.0.0",
"--metrics-host-allowlist='*'",
"--metrics-categories=BEACON,PROCESS,LIBP2P,JVM,NETWORK,PROCESS",
"--metrics-port={0}".format(METRICS_PORT_NUM),
# ^^^^^^^^^^^^^^^^^^^ METRICS CONFIG ^^^^^^^^^^^^^^^^^^^^^
]
if boot_cl_client_ctx != None:
cmd.append("--p2p-discovery-bootnodes="+boot_cl_client_ctx.enr)
if mev_boost_context != None:
cmd.append("--validators-builder-registration-default-enabled=true")
cmd.append("--builder-endpoint='{0}'".format(mev_boost_context_module.mev_boost_endpoint(mev_boost_context)))
if len(extra_params) > 0:
# we do the list comprehension as the default extra_params is a proto repeated string
cmd.extend([param for param in extra_params])
cmd_str = " ".join(cmd)
return ServiceConfig(
image = image,
ports = USED_PORTS,
cmd = [cmd_str],
entrypoint = ENTRYPOINT_ARGS,
files = {
GENESIS_DATA_MOUNT_DIRPATH_ON_SERVICE_CONTAINER: genesis_data.files_artifact_uuid,
VALIDATOR_KEYS_DIRPATH_ON_SERVICE_CONTAINER: node_keystore_files.files_artifact_uuid,
},
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def new_teku_launcher(cl_genesis_data):
return struct(
cl_genesis_data = cl_genesis_data
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
el_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/el_client_context.star")
el_admin_node_info = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/el_admin_node_info.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
# The dirpath of the execution data directory on the client container
EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER = "/opt/besu/execution-data"
GENESIS_DATA_DIRPATH_ON_CLIENT_CONTAINER = "/opt/besu/genesis"
RPC_PORT_NUM = 8545
WS_PORT_NUM = 8546
DISCOVERY_PORT_NUM = 30303
ENGINE_HTTP_RPC_PORT_NUM = 8550
ENGINE_WS_RPC_PORT_NUM = 8551
# Port IDs
RPC_PORT_ID = "rpc"
WS_PORT_ID = "ws"
TCP_DISCOVERY_PORT_ID = "tcp-discovery"
UDP_DISCOVERY_PORT_ID = "udp-discovery"
ENGINE_HTTP_RPC_PORT_ID = "engineHttpRpc"
ENGINE_WS_RPC_PORT_ID = "engineWsRpc"
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
USED_PORTS = {
RPC_PORT_ID: shared_utils.new_port_spec(RPC_PORT_NUM, shared_utils.TCP_PROTOCOL),
WS_PORT_ID: shared_utils.new_port_spec(WS_PORT_NUM, shared_utils.TCP_PROTOCOL),
TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.TCP_PROTOCOL),
UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.UDP_PROTOCOL),
ENGINE_HTTP_RPC_PORT_ID: shared_utils.new_port_spec(ENGINE_HTTP_RPC_PORT_NUM, shared_utils.TCP_PROTOCOL),
ENGINE_WS_RPC_PORT_ID: shared_utils.new_port_spec(ENGINE_WS_RPC_PORT_NUM, shared_utils.TCP_PROTOCOL)
}
ENTRYPOINT_ARGS = ["sh", "-c"]
BESU_LOG_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "ERROR",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "WARN",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "INFO",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "DEBUG",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "TRACE",
}
def launch(
plan,
launcher,
service_name,
image,
participant_log_level,
global_log_level,
existing_el_clients,
extra_params):
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, BESU_LOG_LEVELS)
config = get_config(launcher.network_id, launcher.el_genesis_data,
image, existing_el_clients, log_level, extra_params)
service = plan.add_service(service_name, config)
enode = el_admin_node_info.get_enode_for_node(plan, service_name, RPC_PORT_ID)
return el_client_context.new_el_client_context(
"besu",
"", # besu has no ENR
enode,
service.ip_address,
RPC_PORT_NUM,
WS_PORT_NUM,
ENGINE_HTTP_RPC_PORT_NUM
)
def get_config(network_id, genesis_data, image, existing_el_clients, log_level, extra_params):
if len(existing_el_clients) < 2:
fail("Besu node cannot be boot nodes, and due to a bug it requires two nodes to exist beforehand")
boot_node_1 = existing_el_clients[0]
boot_node_2 = existing_el_clients[1]
genesis_json_filepath_on_client = shared_utils.path_join(GENESIS_DATA_DIRPATH_ON_CLIENT_CONTAINER, genesis_data.besu_genesis_json_relative_filepath)
jwt_secret_json_filepath_on_client = shared_utils.path_join(GENESIS_DATA_DIRPATH_ON_CLIENT_CONTAINER, genesis_data.jwt_secret_relative_filepath)
launch_node_command = [
"besu",
"--logging=" + log_level,
"--data-path=" + EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER,
"--genesis-file=" + genesis_json_filepath_on_client,
"--network-id=" + network_id,
"--host-allowlist=*",
"--rpc-http-enabled=true",
"--rpc-http-host=0.0.0.0",
"--rpc-http-port={0}".format(RPC_PORT_NUM),
"--rpc-http-api=ADMIN,CLIQUE,ETH,NET,DEBUG,TXPOOL,ENGINE",
"--rpc-http-cors-origins=*",
"--rpc-ws-enabled=true",
"--rpc-ws-host=0.0.0.0",
"--rpc-ws-port={0}".format(WS_PORT_NUM),
"--rpc-ws-api=ADMIN,CLIQUE,ETH,NET,DEBUG,TXPOOL,ENGINE",
"--p2p-enabled=true",
"--p2p-host=" + PRIVATE_IP_ADDRESS_PLACEHOLDER,
"--p2p-port={0}".format(DISCOVERY_PORT_NUM),
"--engine-rpc-enabled=true",
"--engine-jwt-secret={0}".format(jwt_secret_json_filepath_on_client),
"--engine-host-allowlist=*",
"--engine-rpc-port={0}".format(ENGINE_HTTP_RPC_PORT_NUM),
]
if len(existing_el_clients) > 0:
launch_node_command.append("--bootnodes={0},{1}".format(boot_node_1.enode, boot_node_2.enode))
if len(extra_params) > 0:
# we do this as extra_params isn't a normal [] but a proto repeated array
launch_node_command.extend([param for param in extra_params])
launch_node_command_str = " ".join(launch_node_command)
return ServiceConfig(
image = image,
ports = USED_PORTS,
cmd = [launch_node_command_str],
files = {
GENESIS_DATA_DIRPATH_ON_CLIENT_CONTAINER: genesis_data.files_artifact_uuid
},
entrypoint = ENTRYPOINT_ARGS,
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def new_besu_launcher(network_id, el_genesis_data):
return struct(
network_id = network_id,
el_genesis_data = el_genesis_data
)
def get_enode_enr_for_node(plan, service_name, port_id):
recipe = PostHttpRequestRecipe(
endpoint = "",
body = '{"method":"admin_nodeInfo","params":[],"id":1,"jsonrpc":"2.0"}',
content_type = "application/json",
port_id = port_id,
extract = {
"enode": ".result.enode",
"enr": ".result.enr",
}
)
response = plan.wait(recipe = recipe, field = "extract.enode", assertion = "!=", target_value = "", timeout = "15m", service_name = service_name)
return (response["extract.enode"], response["extract.enr"])
def get_enode_for_node(plan, service_name, port_id):
recipe = PostHttpRequestRecipe(
endpoint = "",
body = '{"method":"admin_nodeInfo","params":[],"id":1,"jsonrpc":"2.0"}',
content_type = "application/json",
port_id = port_id,
extract = {
"enode": ".result.enode",
}
)
response = plan.wait(recipe = recipe, field = "extract.enode", assertion = "!=", target_value = "", timeout = "15m", service_name = service_name)
return response["extract.enode"]
def new_el_client_context(client_name, enr, enode, ip_addr, rpc_port_num, ws_port_num, engine_rpc_port_num):
return struct(
client_name = client_name,
enr = enr,
enode = enode,
ip_addr = ip_addr,
rpc_port_num = rpc_port_num,
ws_port_num = ws_port_num,
engine_rpc_port_num = engine_rpc_port_num
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
el_admin_node_info = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/el_admin_node_info.star")
el_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/el_client_context.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
# The dirpath of the execution data directory on the client container
EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER = "/home/erigon/execution-data"
GENESIS_DATA_MOUNT_DIRPATH = "/genesis"
RPC_PORT_NUM = 8545
WS_PORT_NUM = 8546
DISCOVERY_PORT_NUM = 30303
ENGINE_RPC_PORT_NUM = 8550
# Port IDs
RPC_PORT_ID = "rpc"
WS_PORT_ID = "ws"
TCP_DISCOVERY_PORT_ID = "tcp-discovery"
UDP_DISCOVERY_PORT_ID = "udp-discovery"
ENGINE_RPC_PORT_ID = "engine-rpc"
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
USED_PORTS = {
RPC_PORT_ID: shared_utils.new_port_spec(RPC_PORT_NUM, shared_utils.TCP_PROTOCOL),
WS_PORT_ID: shared_utils.new_port_spec(WS_PORT_NUM, shared_utils.TCP_PROTOCOL),
TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.TCP_PROTOCOL),
UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.UDP_PROTOCOL),
}
ENTRYPOINT_ARGS = ["sh", "-c"]
ERIGON_LOG_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "1",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "2",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "3",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "4",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "5",
}
def launch(
plan,
launcher,
service_name,
image,
participant_log_level,
global_log_level,
existing_el_clients,
extra_params):
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, ERIGON_LOG_LEVELS)
config = get_config(launcher.network_id, launcher.el_genesis_data,
image, existing_el_clients, log_level, extra_params)
service = plan.add_service(service_name, config)
enode, enr = el_admin_node_info.get_enode_enr_for_node(plan, service_name, RPC_PORT_ID)
return el_client_context.new_el_client_context(
"erigon",
enr,
enode,
service.ip_address,
RPC_PORT_NUM,
WS_PORT_NUM,
ENGINE_RPC_PORT_NUM
)
def get_config(network_id, genesis_data, image, existing_el_clients, verbosity_level, extra_params):
network_id = network_id
genesis_json_filepath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH, genesis_data.erigon_genesis_json_relative_filepath)
jwt_secret_json_filepath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH, genesis_data.jwt_secret_relative_filepath)
init_datadir_cmd_str = "erigon init --datadir={0} {1}".format(
EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER,
genesis_json_filepath_on_client,
)
# TODO remove this based on https://github.com/kurtosis-tech/eth2-merge-kurtosis-module/issues/152
if len(existing_el_clients) == 0:
fail("Erigon needs at least one node to exist, which it treats as the bootnode")
boot_node = existing_el_clients[0]
launch_node_cmd = [
"erigon",
"--log.console.verbosity=" + verbosity_level,
"--datadir=" + EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER,
"--networkid=" + network_id,
"--http",
"--http.addr=0.0.0.0",
"--http.corsdomain=*",
# WARNING: The admin info endpoint is enabled so that we can easily get ENR/enode, which means
# that users should NOT store private information in these Kurtosis nodes!
"--http.api=admin,engine,net,eth",
"--ws",
"--allow-insecure-unlock",
"--nat=extip:" + PRIVATE_IP_ADDRESS_PLACEHOLDER,
"--authrpc.jwtsecret={0}".format(jwt_secret_json_filepath_on_client),
"--nodiscover",
"--staticpeers={0}".format(boot_node.enode),
]
if len(extra_params) > 0:
# this is a repeated<proto type>, we convert it into Starlark
launch_node_cmd.extend([param for param in extra_params])
command_arg = [
init_datadir_cmd_str,
" ".join(launch_node_cmd)
]
command_arg_str = " && ".join(command_arg)
return ServiceConfig(
image = image,
ports = USED_PORTS,
cmd = [command_arg_str],
files = {
GENESIS_DATA_MOUNT_DIRPATH: genesis_data.files_artifact_uuid
},
entrypoint = ENTRYPOINT_ARGS,
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def new_erigon_launcher(network_id, el_genesis_data):
return struct(
network_id = network_id,
el_genesis_data = el_genesis_data,
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
el_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/el_client_context.star")
el_admin_node_info = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/el_admin_node_info.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
RPC_PORT_NUM = 8545
WS_PORT_NUM = 8546
DISCOVERY_PORT_NUM = 30303
ENGINE_RPC_PORT_NUM = 8551
# Port IDs
RPC_PORT_ID = "rpc"
WS_PORT_ID = "ws"
TCP_DISCOVERY_PORT_ID = "tcp-discovery"
UDP_DISCOVERY_PORT_ID = "udp-discovery"
ENGINE_RPC_PORT_ID = "engine-rpc"
ENGINE_WS_PORT_ID = "engineWs"
GENESIS_DATA_MOUNT_DIRPATH = "/genesis"
PREFUNDED_KEYS_MOUNT_DIRPATH = "/prefunded-keys"
# The dirpath of the execution data directory on the client container
EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER = "/execution-data"
KEYSTORE_DIRPATH_ON_CLIENT_CONTAINER = EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER + "/keystore"
GETH_ACCOUNT_PASSWORD = "password" # Password that the Geth accounts will be locked with
GETH_ACCOUNT_PASSWORDS_FILE = "/tmp/password.txt" # Importing an account to
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
USED_PORTS = {
RPC_PORT_ID: shared_utils.new_port_spec(RPC_PORT_NUM, shared_utils.TCP_PROTOCOL),
WS_PORT_ID: shared_utils.new_port_spec(WS_PORT_NUM, shared_utils.TCP_PROTOCOL),
TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.TCP_PROTOCOL),
UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.UDP_PROTOCOL),
ENGINE_RPC_PORT_ID: shared_utils.new_port_spec(ENGINE_RPC_PORT_NUM, shared_utils.TCP_PROTOCOL)
}
ENTRYPOINT_ARGS = ["sh", "-c"]
VERBOSITY_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "1",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "2",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "3",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "4",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "5",
}
def launch(
plan,
launcher,
service_name,
image,
participant_log_level,
global_log_level,
# If empty then the node will be launched as a bootnode
existing_el_clients,
extra_params):
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, VERBOSITY_LEVELS)
config = get_config(launcher.network_id, launcher.el_genesis_data, launcher.prefunded_geth_keys_artifact_uuid,
launcher.prefunded_account_info, image, existing_el_clients, log_level, extra_params)
service = plan.add_service(service_name, config)
enode, enr = el_admin_node_info.get_enode_enr_for_node(plan, service_name, RPC_PORT_ID)
return el_client_context.new_el_client_context(
"geth",
enr,
enode,
service.ip_address,
RPC_PORT_NUM,
WS_PORT_NUM,
ENGINE_RPC_PORT_NUM
)
def get_config(network_id, genesis_data, prefunded_geth_keys_artifact_uuid, prefunded_account_info, image, existing_el_clients, verbosity_level, extra_params):
genesis_json_filepath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH, genesis_data.geth_genesis_json_relative_filepath)
jwt_secret_json_filepath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH, genesis_data.jwt_secret_relative_filepath)
account_addresses_to_unlock = []
for prefunded_account in prefunded_account_info:
account_addresses_to_unlock.append(prefunded_account.address)
accounts_to_unlock_str = ",".join(account_addresses_to_unlock)
init_datadir_cmd_str = "geth init --datadir={0} {1}".format(
EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER,
genesis_json_filepath_on_client,
)
# We need to put the keys into the right spot
copy_keys_into_keystore_cmd_str = "cp -r {0}/* {1}/".format(
PREFUNDED_KEYS_MOUNT_DIRPATH,
KEYSTORE_DIRPATH_ON_CLIENT_CONTAINER,
)
create_passwords_file_cmd_str = '{' + ' for i in $(seq 1 {0}); do echo "{1}" >> {2}; done; '.format(
len(prefunded_account_info),
GETH_ACCOUNT_PASSWORD,
GETH_ACCOUNT_PASSWORDS_FILE,
) + '}'
launch_node_cmd = [
"geth",
"--verbosity=" + verbosity_level,
"--unlock=" + accounts_to_unlock_str,
"--password=" + GETH_ACCOUNT_PASSWORDS_FILE,
"--datadir=" + EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER,
"--networkid=" + network_id,
"--http",
"--http.addr=0.0.0.0",
"--http.vhosts=*",
"--http.corsdomain=*",
# WARNING: The admin info endpoint is enabled so that we can easily get ENR/enode, which means
# that users should NOT store private information in these Kurtosis nodes!
"--http.api=admin,engine,net,eth",
"--ws",
"--ws.addr=0.0.0.0",
"--ws.port={0}".format(WS_PORT_NUM),
"--ws.api=engine,net,eth",
"--ws.origins=*",
"--allow-insecure-unlock",
"--nat=extip:" + PRIVATE_IP_ADDRESS_PLACEHOLDER,
"--verbosity=" + verbosity_level,
"--authrpc.port={0}".format(ENGINE_RPC_PORT_NUM),
"--authrpc.addr=0.0.0.0",
"--authrpc.vhosts=*",
"--authrpc.jwtsecret={0}".format(jwt_secret_json_filepath_on_client),
"--syncmode=full",
]
bootnode_enode = ""
if len(existing_el_clients) > 0:
bootnode_context = existing_el_clients[0]
bootnode_enode = bootnode_context.enode
launch_node_cmd.append(
'--bootnodes="{0}"'.format(bootnode_enode),
)
if len(extra_params) > 0:
# this is a repeated<proto type>, we convert it into Starlark
launch_node_cmd.extend([param for param in extra_params])
launch_node_cmd_str = " ".join(launch_node_cmd)
subcommand_strs = [
init_datadir_cmd_str,
copy_keys_into_keystore_cmd_str,
create_passwords_file_cmd_str,
launch_node_cmd_str,
]
command_str = " && ".join(subcommand_strs)
return ServiceConfig(
image = image,
ports = USED_PORTS,
cmd = [command_str],
files = {
GENESIS_DATA_MOUNT_DIRPATH: genesis_data.files_artifact_uuid,
PREFUNDED_KEYS_MOUNT_DIRPATH: prefunded_geth_keys_artifact_uuid
},
entrypoint = ENTRYPOINT_ARGS,
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER
)
def new_geth_launcher(network_id, el_genesis_data, prefunded_geth_keys_artifact_uuid, prefunded_account_info):
return struct(
network_id = network_id,
el_genesis_data = el_genesis_data,
prefunded_account_info = prefunded_account_info,
prefunded_geth_keys_artifact_uuid = prefunded_geth_keys_artifact_uuid,
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
parse_input = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/parse_input.star")
el_client_context = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/el_client_context.star")
el_admin_node_info = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/el_admin_node_info.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
# The dirpath of the execution data directory on the client container
EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER = "/execution-data"
GENESIS_DATA_MOUNT_DIRPATH = "/genesis"
RPC_PORT_NUM = 8545
WS_PORT_NUM = 8546
DISCOVERY_PORT_NUM = 30303
ENGINE_RPC_PORT_NUM = 8551
# Port IDs
RPC_PORT_ID = "rpc"
WS_PORT_ID = "ws"
TCP_DISCOVERY_PORT_ID = "tcp-discovery"
UDP_DISCOVERY_PORT_ID = "udp-discovery"
ENGINE_RPC_PORT_ID = "engine-rpc"
PRIVATE_IP_ADDRESS_PLACEHOLDER = "KURTOSIS_IP_ADDR_PLACEHOLDER"
USED_PORTS = {
RPC_PORT_ID: shared_utils.new_port_spec(RPC_PORT_NUM, shared_utils.TCP_PROTOCOL),
WS_PORT_ID: shared_utils.new_port_spec(WS_PORT_NUM, shared_utils.TCP_PROTOCOL),
TCP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.TCP_PROTOCOL),
UDP_DISCOVERY_PORT_ID: shared_utils.new_port_spec(DISCOVERY_PORT_NUM, shared_utils.UDP_PROTOCOL),
ENGINE_RPC_PORT_ID: shared_utils.new_port_spec(ENGINE_RPC_PORT_NUM, shared_utils.TCP_PROTOCOL)
}
NETHERMIND_LOG_LEVELS = {
package_io.GLOBAL_CLIENT_LOG_LEVEL.error: "ERROR",
package_io.GLOBAL_CLIENT_LOG_LEVEL.warn: "WARN",
package_io.GLOBAL_CLIENT_LOG_LEVEL.info: "INFO",
package_io.GLOBAL_CLIENT_LOG_LEVEL.debug: "DEBUG",
package_io.GLOBAL_CLIENT_LOG_LEVEL.trace: "TRACE",
}
def launch(
plan,
launcher,
service_name,
image,
participant_log_level,
global_log_level,
existing_el_clients,
extra_params):
log_level = parse_input.get_client_log_level_or_default(participant_log_level, global_log_level, NETHERMIND_LOG_LEVELS)
config = get_config(launcher.el_genesis_data, image, existing_el_clients, log_level, extra_params)
service = plan.add_service(service_name, config)
enode = el_admin_node_info.get_enode_for_node(plan, service_name, RPC_PORT_ID)
return el_client_context.new_el_client_context(
"nethermind",
"", # nethermind has no ENR in the eth2-merge-kurtosis-module either
# Nethermind node info endpoint doesn't return ENR field https://docs.nethermind.io/nethermind/ethereum-client/json-rpc/admin
enode,
service.ip_address,
RPC_PORT_NUM,
WS_PORT_NUM,
ENGINE_RPC_PORT_NUM,
)
def get_config(genesis_data, image, existing_el_clients, log_level, extra_params):
if len(existing_el_clients) < 2:
fail("Nethermind node cannot be boot nodes, and due to a bug it requires two nodes to exist beforehand")
bootnode_1 = existing_el_clients[0]
bootnode_2 = existing_el_clients[1]
genesis_json_filepath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH, genesis_data.nethermind_genesis_json_relative_filepath)
jwt_secret_json_filepath_on_client = shared_utils.path_join(GENESIS_DATA_MOUNT_DIRPATH, genesis_data.jwt_secret_relative_filepath)
command_args = [
"--log=" + log_level,
"--datadir=" + EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER,
"--Init.ChainSpecPath=" + genesis_json_filepath_on_client,
"--Init.WebSocketsEnabled=true",
"--Init.DiagnosticMode=None",
"--JsonRpc.Enabled=true",
"--JsonRpc.EnabledModules=net,eth,consensus,subscribe,web3,admin",
"--JsonRpc.Host=0.0.0.0",
"--JsonRpc.Port={0}".format(RPC_PORT_NUM),
"--JsonRpc.WebSocketsPort={0}".format(WS_PORT_NUM),
"--Network.ExternalIp={0}".format(PRIVATE_IP_ADDRESS_PLACEHOLDER),
"--Network.LocalIp={0}".format(PRIVATE_IP_ADDRESS_PLACEHOLDER),
"--Network.DiscoveryPort={0}".format(DISCOVERY_PORT_NUM),
"--Network.P2PPort={0}".format(DISCOVERY_PORT_NUM),
"--Merge.Enabled=true",
"--Merge.TerminalTotalDifficulty=0", # merge has happened already
"--Merge.TerminalBlockNumber=null",
"--JsonRpc.JwtSecretFile={0}".format(jwt_secret_json_filepath_on_client),
"--JsonRpc.AdditionalRpcUrls=[\"http://0.0.0.0:{0}|http;ws|net;eth;subscribe;engine;web3;client\"]".format(ENGINE_RPC_PORT_NUM),
"--Network.OnlyStaticPeers=true",
"--Network.StaticPeers={0},{1}".format(
bootnode_1.enode,
bootnode_2.enode,
),
]
if len(extra_params) > 0:
# we do this as extra_params is a repeated proto aray
command_args.extend([param for param in extra_params])
return ServiceConfig(
image = image,
ports = USED_PORTS,
cmd = command_args,
files = {
GENESIS_DATA_MOUNT_DIRPATH: genesis_data.files_artifact_uuid,
},
private_ip_address_placeholder = PRIVATE_IP_ADDRESS_PLACEHOLDER,
)
def new_nethermind_launcher(el_genesis_data):
return struct(
el_genesis_data = el_genesis_data
)
def new_participant(el_client_type, cl_client_type, el_client_context, cl_client_context, mev_boost_context):
return struct(
el_client_type = el_client_type,
cl_client_type = cl_client_type,
el_client_context = el_client_context,
cl_client_context = cl_client_context,
mev_boost_context = mev_boost_context
)
cl_validator_keystores = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/cl_validator_keystores/cl_validator_keystore_generator.star")
el_genesis_data_generator = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/el_genesis/el_genesis_data_generator.star")
cl_genesis_data_generator = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/cl_genesis/cl_genesis_data_generator.star")
mev_boost_launcher_module = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/mev_boost/mev_boost_launcher.star")
static_files = import_module("github.com/kurtosis-tech/eth2-package/src/static_files/static_files.star")
geth = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/geth/geth_launcher.star")
besu = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/besu/besu_launcher.star")
erigon = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/erigon/erigon_launcher.star")
nethermind = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/el/nethermind/nethermind_launcher.star")
lighthouse = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/lighthouse/lighthouse_launcher.star")
lodestar = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/lodestar/lodestar_launcher.star")
nimbus = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/nimbus/nimbus_launcher.star")
prysm = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/prysm/prysm_launcher.star")
teku = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/cl/teku/teku_launcher.star")
genesis_constants = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/genesis_constants/genesis_constants.star")
participant_module = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/participant.star")
package_io = import_module("github.com/kurtosis-tech/eth2-package/src/package_io/constants.star")
CL_CLIENT_SERVICE_NAME_PREFIX = "cl-client-"
EL_CLIENT_SERVICE_NAME_PREFIX = "el-client-"
MEV_BOOST_SERVICE_NAME_PREFIX = "mev-boost-"
BOOT_PARTICIPANT_INDEX = 0
# The time that the CL genesis generation step takes to complete, based off what we've seen
CL_GENESIS_DATA_GENERATION_TIME = 2 * time.minute
# Each CL node takes about this time to start up and start processing blocks, so when we create the CL
# genesis data we need to set the genesis timestamp in the future so that nodes don't miss important slots
# (e.g. Altair fork)
# TODO(old) Make this client-specific (currently this is Nimbus)
CL_NODE_STARTUP_TIME = 45 * time.second
MEV_BOOST_SHOULD_CHECK_RELAY = True
CL_CLIENT_CONTEXT_BOOTNODE = None
def launch_participant_network(plan, participants, network_params, global_log_level):
num_participants = len(participants)
plan.print("Generating cl validator key stores")
cl_validator_data = cl_validator_keystores.generate_cl_validator_keystores(
plan,
network_params.preregistered_validator_keys_mnemonic,
num_participants,
network_params.num_validator_keys_per_node,
)
plan.print(json.indent(json.encode(cl_validator_data)))
# We need to send the same genesis time to both the EL and the CL to ensure that timestamp based forking works as expected
final_genesis_timestamp = (time.now() + CL_GENESIS_DATA_GENERATION_TIME + num_participants*CL_NODE_STARTUP_TIME).unix
plan.print("Generating EL data")
el_genesis_generation_config_template = read_file(static_files.EL_GENESIS_GENERATION_CONFIG_TEMPLATE_FILEPATH)
el_genesis_data = el_genesis_data_generator.generate_el_genesis_data(
plan,
el_genesis_generation_config_template,
final_genesis_timestamp,
network_params.network_id,
network_params.deposit_contract_address,
network_params.genesis_delay,
network_params.capella_fork_epoch
)
plan.print(json.indent(json.encode(el_genesis_data)))
plan.print("Uploading GETH prefunded keys")
geth_prefunded_keys_artifact_name = plan.upload_files(static_files.GETH_PREFUNDED_KEYS_DIRPATH, name="geth-prefunded-keys")
plan.print("Uploaded GETH files succesfully, launching EL participants")
el_launchers = {
package_io.EL_CLIENT_TYPE.geth : {"launcher": geth.new_geth_launcher(network_params.network_id, el_genesis_data, geth_prefunded_keys_artifact_name, genesis_constants.PRE_FUNDED_ACCOUNTS), "launch_method": geth.launch},
package_io.EL_CLIENT_TYPE.besu : {"launcher": besu.new_besu_launcher(network_params.network_id, el_genesis_data), "launch_method": besu.launch},
package_io.EL_CLIENT_TYPE.erigon : {"launcher": erigon.new_erigon_launcher(network_params.network_id, el_genesis_data), "launch_method": erigon.launch},
package_io.EL_CLIENT_TYPE.nethermind : {"launcher": nethermind.new_nethermind_launcher(el_genesis_data), "launch_method": nethermind.launch},
}
all_el_client_contexts = []
for index, participant in enumerate(participants):
el_client_type = participant.el_client_type
if el_client_type not in el_launchers:
fail("Unsupported launcher '{0}', need one of '{1}'".format(el_client_type, ",".join([el.name for el in el_launchers.keys()])))
el_launcher, launch_method = el_launchers[el_client_type]["launcher"], el_launchers[el_client_type]["launch_method"]
el_service_name = "{0}{1}".format(EL_CLIENT_SERVICE_NAME_PREFIX, index)
el_client_context = launch_method(
plan,
el_launcher,
el_service_name,
participant.el_client_image,
participant.el_client_log_level,
global_log_level,
all_el_client_contexts,
participant.el_extra_params
)
all_el_client_contexts.append(el_client_context)
plan.print("Succesfully added {0} EL participants".format(num_participants))
plan.print("Generating CL data")
genesis_generation_config_yml_template = read_file(static_files.CL_GENESIS_GENERATION_CONFIG_TEMPLATE_FILEPATH)
genesis_generation_mnemonics_yml_template = read_file(static_files.CL_GENESIS_GENERATION_MNEMONICS_TEMPLATE_FILEPATH)
total_number_of_validator_keys = network_params.num_validator_keys_per_node * num_participants
cl_genesis_data = cl_genesis_data_generator.generate_cl_genesis_data(
plan,
genesis_generation_config_yml_template,
genesis_generation_mnemonics_yml_template,
el_genesis_data,
final_genesis_timestamp,
network_params.network_id,
network_params.deposit_contract_address,
network_params.seconds_per_slot,
network_params.preregistered_validator_keys_mnemonic,
total_number_of_validator_keys,
network_params.genesis_delay,
network_params.capella_fork_epoch
)
plan.print(json.indent(json.encode(cl_genesis_data)))
plan.print("Launching CL network")
cl_launchers = {
package_io.CL_CLIENT_TYPE.lighthouse : {"launcher": lighthouse.new_lighthouse_launcher(cl_genesis_data), "launch_method": lighthouse.launch},
package_io.CL_CLIENT_TYPE.lodestar: {"launcher": lodestar.new_lodestar_launcher(cl_genesis_data), "launch_method": lodestar.launch},
package_io.CL_CLIENT_TYPE.nimbus: {"launcher": nimbus.new_nimbus_launcher(cl_genesis_data), "launch_method": nimbus.launch},
package_io.CL_CLIENT_TYPE.prysm: {"launcher": prysm.new_prysm_launcher(cl_genesis_data, cl_validator_data.prysm_password_relative_filepath, cl_validator_data.prysm_password_artifact_uuid), "launch_method": prysm.launch},
package_io.CL_CLIENT_TYPE.teku: {"launcher": teku.new_teku_launcher(cl_genesis_data), "launch_method": teku.launch},
}
all_cl_client_contexts = []
all_mevboost_contexts = []
preregistered_validator_keys_for_nodes = cl_validator_data.per_node_keystores
for index, participant in enumerate(participants):
cl_client_type = participant.cl_client_type
if cl_client_type not in cl_launchers:
fail("Unsupported launcher '{0}', need one of '{1}'".format(cl_client_type, ",".join([cl.name for cl in cl_launchers.keys()])))
cl_launcher, launch_method = cl_launchers[cl_client_type]["launcher"], cl_launchers[cl_client_type]["launch_method"]
cl_service_name = "{0}{1}".format(CL_CLIENT_SERVICE_NAME_PREFIX, index)
new_cl_node_validator_keystores = preregistered_validator_keys_for_nodes[index]
el_client_context = all_el_client_contexts[index]
mev_boost_context = None
if hasattr(participant, "builder_network_params") and participant.builder_network_params != None:
mev_boost_launcher = mev_boost_launcher_module.new_mev_boost_launcher(MEV_BOOST_SHOULD_CHECK_RELAY, participant.builder_network_params.relay_endpoints)
mev_boost_service_name = MEV_BOOST_SERVICE_NAME_PREFIX.format(1)
mev_boost_context = mev_boost_launcher_module.launch_mevboost(plan, mev_boost_launcher, mev_boost_service_name, network_params.network_id)
all_mevboost_contexts.append(mev_boost_context)
cl_client_context = None
if index == 0:
cl_client_context = launch_method(
plan,
cl_launcher,
cl_service_name,
participant.cl_client_image,
participant.cl_client_log_level,
global_log_level,
CL_CLIENT_CONTEXT_BOOTNODE,
el_client_context,
mev_boost_context,
new_cl_node_validator_keystores,
participant.beacon_extra_params,
participant.validator_extra_params
)
else:
boot_cl_client_ctx = all_cl_client_contexts[0]
cl_client_context = launch_method(
plan,
cl_launcher,
cl_service_name,
participant.cl_client_image,
participant.cl_client_log_level,
global_log_level,
boot_cl_client_ctx,
el_client_context,
mev_boost_context,
new_cl_node_validator_keystores,
participant.beacon_extra_params,
participant.validator_extra_params
)
all_cl_client_contexts.append(cl_client_context)
plan.print("Succesfully added {0} CL participants".format(num_participants))
all_participants = []
for index, participant in enumerate(participants):
el_client_type = participant.el_client_type
cl_client_type = participant.cl_client_type
el_client_context = all_el_client_contexts[index]
cl_client_context = all_cl_client_contexts[index]
mev_boost_context = all_mevboost_contexts[index]
participant_entry = participant_module.new_participant(el_client_type, cl_client_type, el_client_context, cl_client_context, mev_boost_context)
all_participants.append(participant_entry)
return all_participants, final_genesis_timestamp
def new_cl_genesis_data(
files_artifact_uuid,
jwt_secret_rel_filepath,
config_yml_rel_filepath,
genesis_ssz_rel_filepath):
return struct(
files_artifact_uuid = files_artifact_uuid,
jwt_secret_rel_filepath = jwt_secret_rel_filepath,
config_yml_rel_filepath = config_yml_rel_filepath,
genesis_ssz_rel_filepath = genesis_ssz_rel_filepath,
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
cl_genesis_data = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/cl_genesis/cl_genesis_data.star")
prelaunch_data_generator_launcher = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/prelaunch_data_generator_launcher/prelaunch_data_generator_launcher.star")
# Needed to copy the JWT secret and the EL genesis.json file
EL_GENESIS_DIRPATH_ON_GENERATOR = "/el-genesis"
CONFIG_DIRPATH_ON_GENERATOR = "/config"
GENESIS_CONFIG_YML_FILENAME = "config.yaml" # WARNING: Do not change this! It will get copied to the CL genesis data, and the CL clients are hardcoded to look for this filename
MNEMONICS_YML_FILENAME = "mnemonics.yaml"
OUTPUT_DIRPATH_ON_GENERATOR = "/output"
TRANCHES_DIRANME = "tranches"
GENESIS_STATE_FILENAME = "genesis.ssz"
DEPLOY_BLOCK_FILENAME = "deploy_block.txt"
DEPOSIT_CONTRACT_FILENAME = "deposit_contract.txt"
# Generation constants
CL_GENESIS_GENERATION_BINARY_FILEPATH_ON_CONTAINER = "/usr/local/bin/eth2-testnet-genesis"
DEPLOY_BLOCK = "0"
ETH1_BLOCK = "0x0000000000000000000000000000000000000000000000000000000000000000"
SUCCESSFUL_EXEC_CMD_EXIT_CODE = 0
def generate_cl_genesis_data(
plan,
genesis_generation_config_yml_template,
genesis_generation_mnemonics_yml_template,
el_genesis_data,
genesis_unix_timestamp,
network_id,
deposit_contract_address,
seconds_per_slot,
preregistered_validator_keys_mnemonic,
total_num_validator_keys_to_preregister,
genesis_delay,
capella_fork_epoch
):
template_data = new_cl_genesis_config_template_data(
network_id,
seconds_per_slot,
genesis_unix_timestamp,
total_num_validator_keys_to_preregister,
preregistered_validator_keys_mnemonic,
deposit_contract_address,
genesis_delay,
capella_fork_epoch
)
genesis_generation_mnemonics_template_and_data = shared_utils.new_template_and_data(genesis_generation_mnemonics_yml_template, template_data)
genesis_generation_config_template_and_data = shared_utils.new_template_and_data(genesis_generation_config_yml_template, template_data)
template_and_data_by_rel_dest_filepath = {}
template_and_data_by_rel_dest_filepath[MNEMONICS_YML_FILENAME] = genesis_generation_mnemonics_template_and_data
template_and_data_by_rel_dest_filepath[GENESIS_CONFIG_YML_FILENAME] = genesis_generation_config_template_and_data
genesis_generation_config_artifact_name = plan.render_templates(template_and_data_by_rel_dest_filepath, "genesis-generation-config-cl")
# TODO(old) Make this the actual data generator - comment copied from the original module
launcher_service_name = prelaunch_data_generator_launcher.launch_prelaunch_data_generator(
plan,
{
CONFIG_DIRPATH_ON_GENERATOR: genesis_generation_config_artifact_name,
EL_GENESIS_DIRPATH_ON_GENERATOR: el_genesis_data.files_artifact_uuid,
},
)
all_dirpaths_to_create_on_generator = [
CONFIG_DIRPATH_ON_GENERATOR,
OUTPUT_DIRPATH_ON_GENERATOR,
]
all_dirpath_creation_commands = []
for dirpath_to_create_on_generator in all_dirpaths_to_create_on_generator:
all_dirpath_creation_commands.append(
"mkdir -p {0}".format(dirpath_to_create_on_generator))
dir_creation_cmd = [
"bash",
"-c",
(" && ").join(all_dirpath_creation_commands),
]
dir_creation_cmd_result = plan.exec(recipe=ExecRecipe(command=dir_creation_cmd), service_name=launcher_service_name)
plan.assert(dir_creation_cmd_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
# Copy files to output
all_filepaths_to_copy_to_ouptut_directory = [
shared_utils.path_join(CONFIG_DIRPATH_ON_GENERATOR, GENESIS_CONFIG_YML_FILENAME),
shared_utils.path_join(CONFIG_DIRPATH_ON_GENERATOR, MNEMONICS_YML_FILENAME),
shared_utils.path_join(EL_GENESIS_DIRPATH_ON_GENERATOR, el_genesis_data.jwt_secret_relative_filepath),
]
for filepath_on_generator in all_filepaths_to_copy_to_ouptut_directory:
cmd = [
"cp",
filepath_on_generator,
OUTPUT_DIRPATH_ON_GENERATOR,
]
cmd_result = plan.exec(recipe=ExecRecipe( command=cmd), service_name=launcher_service_name)
plan.assert(cmd_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
# Generate files that need dynamic content
content_to_write_to_output_filename = {
DEPLOY_BLOCK: DEPLOY_BLOCK_FILENAME,
deposit_contract_address: DEPOSIT_CONTRACT_FILENAME,
}
for content, destFilename in content_to_write_to_output_filename.items():
destFilepath = shared_utils.path_join(OUTPUT_DIRPATH_ON_GENERATOR, destFilename)
cmd = [
"sh",
"-c",
"echo {0} > {1}".format(
content,
destFilepath,
)
]
cmd_result = plan.exec(recipe=ExecRecipe( command=cmd), service_name=launcher_service_name)
plan.assert(cmd_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
cl_genesis_generation_cmd = [
CL_GENESIS_GENERATION_BINARY_FILEPATH_ON_CONTAINER,
"merge",
"--config", shared_utils.path_join(OUTPUT_DIRPATH_ON_GENERATOR, GENESIS_CONFIG_YML_FILENAME),
"--mnemonics", shared_utils.path_join(OUTPUT_DIRPATH_ON_GENERATOR, MNEMONICS_YML_FILENAME),
"--eth1-config", shared_utils.path_join(EL_GENESIS_DIRPATH_ON_GENERATOR, el_genesis_data.geth_genesis_json_relative_filepath),
"--tranches-dir", shared_utils.path_join(OUTPUT_DIRPATH_ON_GENERATOR, TRANCHES_DIRANME),
"--state-output", shared_utils.path_join(OUTPUT_DIRPATH_ON_GENERATOR, GENESIS_STATE_FILENAME)
]
genesis_generation_result = plan.exec(recipe=ExecRecipe(command=cl_genesis_generation_cmd), service_name=launcher_service_name)
plan.assert(genesis_generation_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
cl_genesis_data_artifact_name = plan.store_service_files(launcher_service_name, OUTPUT_DIRPATH_ON_GENERATOR, name = "cl-genesis-data")
jwt_secret_rel_filepath = shared_utils.path_join(
shared_utils.path_base(OUTPUT_DIRPATH_ON_GENERATOR),
shared_utils.path_base(el_genesis_data.jwt_secret_relative_filepath),
)
genesis_config_rel_filepath = shared_utils.path_join(
shared_utils.path_base(OUTPUT_DIRPATH_ON_GENERATOR),
GENESIS_CONFIG_YML_FILENAME,
)
genesis_ssz_rel_filepath = shared_utils.path_join(
shared_utils.path_base(OUTPUT_DIRPATH_ON_GENERATOR),
GENESIS_STATE_FILENAME,
)
result = cl_genesis_data.new_cl_genesis_data(
cl_genesis_data_artifact_name,
jwt_secret_rel_filepath,
genesis_config_rel_filepath,
genesis_ssz_rel_filepath,
)
# we cleanup as the data generation is done
plan.remove_service(launcher_service_name)
return result
def new_cl_genesis_config_template_data(network_id, seconds_per_slot, unix_timestamp, num_validator_keys_to_preregister, preregistered_validator_keys_mnemonic, deposit_contract_address, genesis_delay, capella_fork_epoch):
return {
"NetworkId": network_id,
"SecondsPerSlot": seconds_per_slot,
"UnixTimestamp": unix_timestamp,
"NumValidatorKeysToPreregister": num_validator_keys_to_preregister,
"PreregisteredValidatorKeysMnemonic": preregistered_validator_keys_mnemonic,
"DepositContractAddress": deposit_contract_address,
"GenesisDelay": genesis_delay,
"CapellaForkEpoch": capella_fork_epoch
}
prelaunch_data_generator_launcher = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/prelaunch_data_generator_launcher/prelaunch_data_generator_launcher.star")
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
keystore_files_module = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/cl_validator_keystores/keystore_files.star")
keystores_result = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/cl_validator_keystores/generate_keystores_result.star")
NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR = "/node-{0}-keystores"
# Prysm keystores are encrypted with a password
PRYSM_PASSWORD = "password"
PRYSM_PASSWORD_FILEPATH_ON_GENERATOR = "/tmp/prysm-password.txt"
KEYSTORES_GENERATION_TOOL_NAME = "eth2-val-tools"
SUCCESSFUL_EXEC_CMD_EXIT_CODE = 0
RAW_KEYS_DIRNAME = "keys"
RAW_SECRETS_DIRNAME = "secrets"
NIMBUS_KEYS_DIRNAME = "nimbus-keys"
PRYSM_DIRNAME = "prysm"
TEKU_KEYS_DIRNAME = "teku-keys"
TEKU_SECRETS_DIRNAME = "teku-secrets"
# Generates keystores for the given number of nodes from the given mnemonic, where each keystore contains approximately
#
# num_keys / num_nodes keys
def generate_cl_validator_keystores(
plan,
mnemonic,
num_nodes,
num_validators_per_node):
service_name = prelaunch_data_generator_launcher.launch_prelaunch_data_generator(
plan,
{},
)
all_output_dirpaths = []
all_sub_command_strs = []
start_index = 0
stop_index = num_validators_per_node
for i in range(num_nodes):
output_dirpath = NODE_KEYSTORES_OUTPUT_DIRPATH_FORMAT_STR.format(i)
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,
output_dirpath,
mnemonic,
start_index,
stop_index,
)
all_sub_command_strs.append(generate_keystores_cmd)
all_output_dirpaths.append(output_dirpath)
start_index = stop_index
stop_index = stop_index + num_validators_per_node
command_str = " && ".join(all_sub_command_strs)
command_result = plan.exec(recipe=ExecRecipe(command=["sh", "-c", command_str]), service_name=service_name)
plan.assert(command_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
# Store outputs into files artifacts
keystore_files = []
for idx, output_dirpath in enumerate(all_output_dirpaths):
artifact_name = plan.store_service_files(service_name, output_dirpath, name = "validator-keystore-" + str(idx))
# This is necessary because the way Kurtosis currently implements artifact-storing is
base_dirname_in_artifact = shared_utils.path_base(output_dirpath)
to_add = keystore_files_module.new_keystore_files(
artifact_name,
shared_utils.path_join(base_dirname_in_artifact, RAW_KEYS_DIRNAME),
shared_utils.path_join(base_dirname_in_artifact, RAW_SECRETS_DIRNAME),
shared_utils.path_join(base_dirname_in_artifact, NIMBUS_KEYS_DIRNAME),
shared_utils.path_join(base_dirname_in_artifact, PRYSM_DIRNAME),
shared_utils.path_join(base_dirname_in_artifact, TEKU_KEYS_DIRNAME),
shared_utils.path_join(base_dirname_in_artifact, TEKU_SECRETS_DIRNAME),
)
keystore_files.append(to_add)
write_prysm_password_file_cmd = [
"sh",
"-c",
"echo '{0}' > {1}".format(
PRYSM_PASSWORD,
PRYSM_PASSWORD_FILEPATH_ON_GENERATOR,
),
]
write_prysm_password_file_cmd_result = plan.exec(recipe=ExecRecipe(command=write_prysm_password_file_cmd), service_name=service_name)
plan.assert(write_prysm_password_file_cmd_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
prysm_password_artifact_name = plan.store_service_files(service_name, PRYSM_PASSWORD_FILEPATH_ON_GENERATOR, name = "prysm-password")
result = keystores_result.new_generate_keystores_result(
prysm_password_artifact_name,
shared_utils.path_base(PRYSM_PASSWORD_FILEPATH_ON_GENERATOR),
keystore_files,
)
# we cleanup as the data generation is done
plan.remove_service(service_name)
return result
# Package object containing information about the keystores that were generated for validators
# during genesis creation
def new_generate_keystores_result(prysm_password_artifact_uuid, prysm_password_relative_filepath, per_node_keystores):
return struct(
#Files artifact UUID where the Prysm password is stored
prysm_password_artifact_uuid = prysm_password_artifact_uuid,
# Relative to root of files artifact
prysm_password_relative_filepath = prysm_password_relative_filepath,
# Contains keystores-per-client-type for each node in the network
per_node_keystores = per_node_keystores
)
# One of these will be created per node we're trying to start
def new_keystore_files(files_artifact_uuid, raw_keys_relative_dirpath, raw_secrets_relative_dirpath, nimbus_keys_relative_dirpath, prysm_relative_dirpath, teku_keys_relative_dirpath, teku_secrets_relative_dirpath):
return struct(
files_artifact_uuid = files_artifact_uuid,
# ------------ All directories below are relative to the root of the files artifact ----------------
raw_keys_relative_dirpath = raw_keys_relative_dirpath,
raw_secrets_relative_dirpath = raw_secrets_relative_dirpath,
nimbus_keys_relative_dirpath = nimbus_keys_relative_dirpath,
prysm_relative_dirpath = prysm_relative_dirpath,
teku_keys_relative_dirpath = teku_keys_relative_dirpath,
teku_secrets_relative_dirpath = teku_secrets_relative_dirpath
)
def new_el_genesis_data(
files_artifact_uuid,
jwt_secret_relative_filepath,
geth_genesis_json_relative_filepath,
erigon_genesis_json_relative_filepath,
nethermind_genesis_json_relative_filepath,
besu_genesis_json_relative_filepath):
return struct(
files_artifact_uuid = files_artifact_uuid,
jwt_secret_relative_filepath = jwt_secret_relative_filepath,
geth_genesis_json_relative_filepath = geth_genesis_json_relative_filepath,
erigon_genesis_json_relative_filepath = erigon_genesis_json_relative_filepath,
nethermind_genesis_json_relative_filepath = nethermind_genesis_json_relative_filepath,
besu_genesis_json_relative_filepath = besu_genesis_json_relative_filepath,
)
shared_utils = import_module("github.com/kurtosis-tech/eth2-package/src/shared_utils/shared_utils.star")
el_genesis = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/el_genesis/el_genesis_data.star")
prelaunch_data_generator_launcher = import_module("github.com/kurtosis-tech/eth2-package/src/participant_network/prelaunch_data_generator/prelaunch_data_generator_launcher/prelaunch_data_generator_launcher.star")
CONFIG_DIRPATH_ON_GENERATOR = "/config"
GENESIS_CONFIG_FILENAME = "genesis-config.yaml"
OUTPUT_DIRPATH_ON_GENERATOR = "/output"
GETH_GENESIS_FILENAME = "genesis.json"
ERIGON_GENESIS_FILENAME = "erigon.json"
NETHERMIND_GENESIS_FILENAME = "nethermind.json"
BESU_GENESIS_FILENAME = "besu.json"
JWT_SECRET_FILENAME = "jwtsecret"
SUCCESSFUL_EXEC_CMD_EXIT_CODE = 0
# Mapping of output genesis filename -> generator to create the file
all_genesis_generation_cmds = {
GETH_GENESIS_FILENAME: lambda genesis_config_filepath_on_generator: ["python3", "/apps/el-gen/genesis_geth.py", genesis_config_filepath_on_generator],
ERIGON_GENESIS_FILENAME: lambda genesis_config_filepath_on_generator: ["python3", "/apps/el-gen/genesis_geth.py",genesis_config_filepath_on_generator],
NETHERMIND_GENESIS_FILENAME: lambda genesis_config_filepath_on_generator: ["python3", "/apps/el-gen/genesis_chainspec.py", genesis_config_filepath_on_generator],
BESU_GENESIS_FILENAME: lambda genesis_config_filepath_on_generator :["python3", "/apps/el-gen/genesis_besu.py", genesis_config_filepath_on_generator]
}
def generate_el_genesis_data(
plan,
genesis_generation_config_template,
genesis_unix_timestamp,
network_id,
deposit_contract_address,
genesis_delay,
capella_fork_epoch
):
template_data = genesis_generation_config_template_data(
network_id,
deposit_contract_address,
genesis_unix_timestamp,
genesis_delay,
capella_fork_epoch
)
genesis_config_file_template_and_data = shared_utils.new_template_and_data(genesis_generation_config_template, template_data)
template_and_data_by_rel_dest_filepath = {}
template_and_data_by_rel_dest_filepath[GENESIS_CONFIG_FILENAME] = genesis_config_file_template_and_data
genesis_generation_config_artifact_name = plan.render_templates(template_and_data_by_rel_dest_filepath, name="genesis-generation-config-el")
# TODO(old) Make this the actual data generator - comment copied from the original module
launcher_service_name = prelaunch_data_generator_launcher.launch_prelaunch_data_generator(
plan,
{
CONFIG_DIRPATH_ON_GENERATOR: genesis_generation_config_artifact_name,
},
)
all_dirpaths_to_create_on_generator = [
CONFIG_DIRPATH_ON_GENERATOR,
OUTPUT_DIRPATH_ON_GENERATOR,
]
all_dirpath_creation_commands = []
for dirpath_to_create_on_generator in all_dirpaths_to_create_on_generator:
all_dirpath_creation_commands.append(
"mkdir -p {0}".format(dirpath_to_create_on_generator),
)
dir_creation_cmd = [
"bash",
"-c",
" && ".join(all_dirpath_creation_commands),
]
dir_creation_cmd_result = plan.exec(recipe=ExecRecipe(command=dir_creation_cmd), service_name=launcher_service_name)
plan.assert(dir_creation_cmd_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
genesis_config_filepath_on_generator = shared_utils.path_join(CONFIG_DIRPATH_ON_GENERATOR, GENESIS_CONFIG_FILENAME)
genesis_filename_to_relative_filepath_in_artifact = {}
for output_filename, generation_cmd in all_genesis_generation_cmds.items():
cmd = generation_cmd(genesis_config_filepath_on_generator)
output_filepath_on_generator = shared_utils.path_join(OUTPUT_DIRPATH_ON_GENERATOR, output_filename)
cmd.append(">")
cmd.append(output_filepath_on_generator)
cmd_to_execute = [
"bash",
"-c",
" ".join(cmd)
]
cmd_to_execute_result = plan.exec(recipe=ExecRecipe(command=cmd_to_execute), service_name=launcher_service_name)
plan.assert(cmd_to_execute_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
genesis_filename_to_relative_filepath_in_artifact[output_filename] = shared_utils.path_join(
shared_utils.path_base(OUTPUT_DIRPATH_ON_GENERATOR),
output_filename,
)
jwt_secret_filepath_on_generator = shared_utils.path_join(OUTPUT_DIRPATH_ON_GENERATOR, JWT_SECRET_FILENAME)
jwt_secret_generation_cmd = [
"bash",
"-c",
"openssl rand -hex 32 | tr -d \"\\n\" | sed 's/^/0x/' > {0}".format(
jwt_secret_filepath_on_generator,
)
]
jwt_secret_generation_cmd_result = plan.exec(recipe=ExecRecipe(command=jwt_secret_generation_cmd), service_name=launcher_service_name)
plan.assert(jwt_secret_generation_cmd_result["code"], "==", SUCCESSFUL_EXEC_CMD_EXIT_CODE)
el_genesis_data_artifact_name = plan.store_service_files(launcher_service_name, OUTPUT_DIRPATH_ON_GENERATOR, name = "el-genesis-data")
result = el_genesis.new_el_genesis_data(
el_genesis_data_artifact_name,
shared_utils.path_join(shared_utils.path_base(OUTPUT_DIRPATH_ON_GENERATOR), JWT_SECRET_FILENAME),
genesis_filename_to_relative_filepath_in_artifact[GETH_GENESIS_FILENAME],
genesis_filename_to_relative_filepath_in_artifact[ERIGON_GENESIS_FILENAME],
genesis_filename_to_relative_filepath_in_artifact[NETHERMIND_GENESIS_FILENAME],
genesis_filename_to_relative_filepath_in_artifact[BESU_GENESIS_FILENAME],
)
# we cleanup as the data generation is done
plan.remove_service(launcher_service_name)
return result
def genesis_generation_config_template_data(network_id, deposit_contract_address, unix_timestamp, genesis_delay, capella_fork_epoch):
return {
"NetworkId": network_id,
"DepositContractAddress": deposit_contract_address,
"UnixTimestamp": unix_timestamp,
"GenesisDelay": genesis_delay,
"CapellaForkEpoch": capella_fork_epoch
}
def new_prefunded_account(address, private_key):
return struct(address = address, private_key = private_key)
# This information was generated by:
# 1) Installing Wagyu: https://github.com/AleoHQ/wagyu
# 2) Running `wagyu ethereum import-hd -m MNEMONIC_FROM_GENESIS -d PREFUNDED_ACCOUNT_DERIVATION_PATH`
# 3) Copying the outputted information
PRE_FUNDED_ACCOUNTS = [
# UTC--2021-12-22T19-14-08.590377700Z--878705ba3f8bc32fcf7f4caa1a35e72af65cf766
# m/44'/60'/0'/0/0
new_prefunded_account(
"0x878705ba3f8Bc32FCf7F4CAa1A35E72AF65CF766",
"ef5177cd0b6b21c87db5a0bf35d4084a8a57a9d6a064f86d51ac85f2b873a4e2",
),
# UTC--2021-12-22T19-14-13.423928600Z--4e9a3d9d1cd2a2b2371b8b3f489ae72259886f1a
# m/44'/60'/0'/0/1
new_prefunded_account(
"0x4E9A3d9D1cd2A2b2371b8b3F489aE72259886f1A",
"48fcc39ae27a0e8bf0274021ae6ebd8fe4a0e12623d61464c498900b28feb567",
),
# UTC--2021-12-22T19-14-16.977667900Z--df8466f277964bb7a0ffd819403302c34dcd530a
# m/44'/60'/0'/0/2
new_prefunded_account(
"0xdF8466f277964Bb7a0FFD819403302C34DCD530A",
"7988b3a148716ff800414935b305436493e1f25237a2a03e5eebc343735e2f31",
),
# UTC--2021-12-22T19-14-21.531351400Z--5c613e39fc0ad91afda24587e6f52192d75fba50
# m/44'/60'/0'/0/3
new_prefunded_account(
"0x5c613e39Fc0Ad91AfDA24587e6f52192d75FBA50",
"b3c409b6b0b3aa5e65ab2dc1930534608239a478106acf6f3d9178e9f9b00b35",
),
# UTC--2021-12-22T19-14-25.369306000Z--375ae6107f8cc4cf34842b71c6f746a362ad8eac
# m/44'/60'/0'/0/4
new_prefunded_account(
"0x375ae6107f8cC4cF34842B71C6F746a362Ad8EAc",
"df9bb6de5d3dc59595bcaa676397d837ff49441d211878c024eabda2cd067c9f",
),
# UTC--2021-12-22T19-14-33.473095100Z--1f6298457c5d76270325b724da5d1953923a6b88
# m/44'/60'/0'/0/5
new_prefunded_account(
"0x1F6298457C5d76270325B724Da5d1953923a6B88",
"7da08f856b5956d40a72968f93396f6acff17193f013e8053f6fbb6c08c194d6",
),
]
IMAGE = "ethpandaops/ethereum-genesis-generator:1.0.17"
SERVICE_NAME_PREFIX = "prelaunch-data-generator-"
# We use Docker exec commands to run the commands we need, so we override the default
ENTRYPOINT_ARGS = [
"sleep",
"999999",
]
# Launches a prelaunch data generator IMAGE, for use in various of the genesis generation
def launch_prelaunch_data_generator(plan, files_artifact_mountpoints):
config = get_config(files_artifact_mountpoints)
service_name = "{0}{1}".format(
SERVICE_NAME_PREFIX,
time.now().unix_nano,
)
plan.add_service(service_name, config)
return service_name
def get_config(
files_artifact_mountpoints,
):
return ServiceConfig(
image = IMAGE,
entrypoint = ENTRYPOINT_ARGS,
files = files_artifact_mountpoints,
)
# The path on the module container where static files are housed
STATIC_FILES_DIRPATH = "github.com/kurtosis-tech/eth2-package/static_files"
# Geth + CL genesis generation
GENESIS_GENERATION_CONFIG_DIRPATH = STATIC_FILES_DIRPATH + "/genesis-generation-config"
EL_GENESIS_GENERATION_CONFIG_DIRPATH = GENESIS_GENERATION_CONFIG_DIRPATH + "/el"
EL_GENESIS_GENERATION_CONFIG_TEMPLATE_FILEPATH = EL_GENESIS_GENERATION_CONFIG_DIRPATH + \
"/genesis-config.yaml.tmpl"
CL_GENESIS_GENERATION_CONFIG_DIRPATH = GENESIS_GENERATION_CONFIG_DIRPATH + "/cl"
CL_GENESIS_GENERATION_CONFIG_TEMPLATE_FILEPATH = CL_GENESIS_GENERATION_CONFIG_DIRPATH + \
"/config.yaml.tmpl"
CL_GENESIS_GENERATION_MNEMONICS_TEMPLATE_FILEPATH = CL_GENESIS_GENERATION_CONFIG_DIRPATH + \
"/mnemonics.yaml.tmpl"
# Prefunded keys
PREFUNDED_KEYS_DIRPATH = STATIC_FILES_DIRPATH + "/genesis-prefunded-keys"
GETH_PREFUNDED_KEYS_DIRPATH = PREFUNDED_KEYS_DIRPATH + "/geth"
# Forkmon config
FORKMON_CONFIG_TEMPLATE_FILEPATH = STATIC_FILES_DIRPATH + \
"/forkmon-config/config.toml.tmpl"
......
# Extends the mainnet preset
# This *could* be 'minimal', but it's not recommended to use because not every client supports 'minimal'
PRESET_BASE: 'mainnet'
CONFIG_NAME: testnet # needs to exist because of Prysm. Otherwise it conflicts with mainnet genesis
# Genesis
# ---------------------------------------------------------------
# `2**14` (= 16,384)
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: {{ .NumValidatorKeysToPreregister }}
MIN_GENESIS_TIME: {{ .UnixTimestamp }}
GENESIS_FORK_VERSION: 0x10000038
GENESIS_DELAY: {{ .GenesisDelay }}
# Forking
# ---------------------------------------------------------------
# Some forks are disabled for now:
# - These may be re-assigned to another fork-version later
# - Temporarily set to max uint64 value: 2**64 - 1
# Note: The module runs a merged chain so ALTAIR_FORK_EPOCH, BELLATRIX_FORK_EPOCH and TERMINAL_TOTAL_DIFFICULTY
# are all hardcoded to zero.
# Altair
ALTAIR_FORK_VERSION: 0x20000038
ALTAIR_FORK_EPOCH: 0
# Merge
BELLATRIX_FORK_VERSION: 0x30000038
BELLATRIX_FORK_EPOCH: 0
TERMINAL_TOTAL_DIFFICULTY: 0
# 0x0000...000 indicates that we use TERMINAL_TOTAL_DIFFICULTY instead of a block has to trigger the merge
TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000
# NOTE: This is commented out because Nimbus warns us that it's an unrecognized parameter
TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615
# Capella
CAPELLA_FORK_VERSION: 0x40000038
CAPELLA_FORK_EPOCH: {{ .CapellaForkEpoch }}
# EIP4844
EIP4844_FORK_VERSION: 0x50000040
EIP4844_FORK_EPOCH: 18446744073709551615
# Time parameters
# ---------------------------------------------------------------
# 12 seconds
SECONDS_PER_SLOT: {{ .SecondsPerSlot }}
# 5 epochs ~0.5 hours
MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 5
# 2**8 (= 256) epochs ~27 hours
SHARD_COMMITTEE_PERIOD: 256
# It's very important that SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE is a good amount of time, else
# jitter will cause the Beacon nodes to think they're far behind the Eth1 nodes and give up syncing
SECONDS_PER_ETH1_BLOCK: 10
ETH1_FOLLOW_DISTANCE: 12
# Validator cycle
# ---------------------------------------------------------------
# 2**2 (= 4)
INACTIVITY_SCORE_BIAS: 4
# 2**4 (= 16)
INACTIVITY_SCORE_RECOVERY_RATE: 16
# 2**4 * 10**9 (= 16,000,000,000) Gwei
EJECTION_BALANCE: 16000000000
# 2**2 (= 4)
MIN_PER_EPOCH_CHURN_LIMIT: 4
# 2**16 (= 65,536)
CHURN_LIMIT_QUOTIENT: 65536
# Fork choice
# ---------------------------------------------------------------
# 40%
PROPOSER_SCORE_BOOST: 40
# Deposit contract
# ---------------------------------------------------------------
DEPOSIT_CHAIN_ID: {{ .NetworkId }}
DEPOSIT_NETWORK_ID: {{ .NetworkId }}
DEPOSIT_CONTRACT_ADDRESS: {{ .DepositContractAddress }}
\ No newline at end of file
- mnemonic: "{{ .PreregisteredValidatorKeysMnemonic }}" # a 24 word BIP 39 mnemonic
# Number of validator keys to preregister inside the outputted CL genesis.ssz
count: {{ .NumValidatorKeysToPreregister }}
\ No newline at end of file
# NOTE: This does NOT have any relevance to the mnemonics & validator keys in the CL genesis!
mnemonic: "stumble horn valley travel milk void screen bulk wink hood cup item glove setup wrong toward erase invite saddle this poverty basket index lab"
el_premine:
"m/44'/60'/0'/0/0": 10000000ETH
"m/44'/60'/0'/0/1": 10000000ETH
"m/44'/60'/0'/0/2": 10000000ETH
"m/44'/60'/0'/0/3": 10000000ETH
"m/44'/60'/0'/0/4": 10000000ETH
"m/44'/60'/0'/0/5": 10000000ETH
el_premine_addrs: {}
chain_id: {{ .NetworkId }}
deposit_contract_address: "{{ .DepositContractAddress }}"
genesis_timestamp: {{ .UnixTimestamp }}
# Note: The module runs a merged chain so terminal_total_difficulty is hardcoded to zero.
terminal_total_difficulty: 0
genesis_delay: {{ .GenesisDelay }}
capella_fork_epoch: {{ .CapellaForkEpoch }}
clique:
enabled: false
\ No newline at end of file
{"address":"878705ba3f8bc32fcf7f4caa1a35e72af65cf766","crypto":{"cipher":"aes-128-ctr","ciphertext":"f02daebbf456faf787c5cd61a33ce780857c1ca10b00972aa451f0e9688e4ead","cipherparams":{"iv":"ef1668814155862f0653f15dae845e58"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"55e5ee70d3e882d2f00a073eda252ff01437abf51d7bfa76c06dcc73f7e8f1a3"},"mac":"d8d04625d0769fe286756734f946c78663961b74f0caaff1d768f0d255632f04"},"id":"5fb9083a-a221-412b-b0e0-921e22cc9645","version":3}
\ No newline at end of file
{"address":"4e9a3d9d1cd2a2b2371b8b3f489ae72259886f1a","crypto":{"cipher":"aes-128-ctr","ciphertext":"ab715382b1e1f13d927b2e3d22e087a51ccb72b32f9bac71727ec8438ecb6d54","cipherparams":{"iv":"dee12212262986854a0bfd9a5c766ced"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"7112f2fed838981fde1ca00cdd1d95630981bd22f772666fafd0778525cf1cc4"},"mac":"2a75795bb0513859355ce2086668398ee821a2e708edd856f8a85cf638fede9a"},"id":"f849b7fe-aff7-454f-91e1-838de2a8da6b","version":3}
\ No newline at end of file
{"address":"df8466f277964bb7a0ffd819403302c34dcd530a","crypto":{"cipher":"aes-128-ctr","ciphertext":"322e59ab95797f2ea9a1162e3f28e2ff7e27415b6e9d7d990a197e09dc9043d7","cipherparams":{"iv":"6179d5971b93a09799ace7371801e371"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"5ac65d7e25467366764d4539e8ae0c78d31dce4002042c06d3971b103c95f2a3"},"mac":"b5d060f2c0a5f8446dd4d718eee66c7eeff3feb90aafa8201fd7501c8f5c180a"},"id":"c5fda7a7-816a-4740-8804-afdc0d410cfb","version":3}
\ No newline at end of file
{"address":"5c613e39fc0ad91afda24587e6f52192d75fba50","crypto":{"cipher":"aes-128-ctr","ciphertext":"4ba38c15225d92f2cbac5eafb7cf5ef358332037cd9730dce595a7a4cc3a39d0","cipherparams":{"iv":"6a83dc5b43b0c9c8948905ccc697455a"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"cdfa581366dbc4f566404b0ac64d9d0c1a8a9d24202fe3c428212f020fd6cdbb"},"mac":"95a2e987cbe87d4b532f830df8c5cabc8a7bbd4e70eda672252ed4d8b967e660"},"id":"09d1b784-fb8f-4d25-8720-a683bb0c13ab","version":3}
\ No newline at end of file
{"address":"375ae6107f8cc4cf34842b71c6f746a362ad8eac","crypto":{"cipher":"aes-128-ctr","ciphertext":"ab13f28ad41bcb73f5ae982a5bde37ba737515fef848ea365911be3d97682530","cipherparams":{"iv":"fb4d9bfab1d9c5d47e46052ea80275e1"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"56ba7f86161cb419f27c19b5845b6dd8033927ff0d362863e5723e3568d5d0c7"},"mac":"ad9197d47c57c601313d49fb15392a29c8a16418d1bf6f39ac3b822bd5205593"},"id":"353e4c37-a37f-4b2a-8729-81460c6a92d4","version":3}
\ No newline at end of file
{"address":"1f6298457c5d76270325b724da5d1953923a6b88","crypto":{"cipher":"aes-128-ctr","ciphertext":"42348bd719f9225cc91184a3daf7005a89cec8be7d907c92c57ac01f29b61e2d","cipherparams":{"iv":"ee8d92dde2c3dc230f1f6e765641e0ce"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"74992f3587a487202cd76d6aa625af6b0e5b2f68eb0ea3647edb4541ce24adb9"},"mac":"27cd4f0aa624fce848aebccbd80efda35d615da2d274cc39e5185170d2ff4017"},"id":"27bfc138-d358-4c21-b040-93458f11e4c4","version":3}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment