From bfaff7699b05eaef86f84e521b3bbd302746ed62 Mon Sep 17 00:00:00 2001 From: Jun Song Date: Wed, 12 Nov 2025 17:40:19 +0900 Subject: [PATCH] Initial support for Zeam --- src/clients/launcher.star | 35 +++++++-- src/clients/zeam/zeam_launcher.star | 114 ++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 src/clients/zeam/zeam_launcher.star diff --git a/src/clients/launcher.star b/src/clients/launcher.star index 9cf6293..75ae20b 100644 --- a/src/clients/launcher.star +++ b/src/clients/launcher.star @@ -3,6 +3,7 @@ Module for launching the PQ devnet. """ ream_launcher = import_module("./ream/ream_launcher.star") +zeam_launcher = import_module("./zeam/zeam_launcher.star") def prelaunch(plan, participants, keys_artifacts): """ @@ -24,13 +25,18 @@ def prelaunch(plan, participants, keys_artifacts): client_type = participant.get("type") client_image = participant.get("image", "") client_count = participant.get("count", 1) - - # TODO: Support other client types - if client_type != "ream": + + # TODO: Add Qlean + if client_type == "ream": + launcher = ream_launcher + elif client_type == "zeam": + launcher = zeam_launcher + else: + plan.print("Unsupported client type: {}".format(client_type)) continue for _ in range(client_count): - service = ream_launcher.initialize( + service = launcher.initialize( plan, client_image, node_index, @@ -77,12 +83,29 @@ def launch(plan, services, genesis_artifacts): }, description = "Reading config.yaml from genesis artifacts", ) + validator_config_yaml_result = plan.run_sh( + run = "cat /genesis/validator-config.yaml", + files = { + "/genesis": genesis_artifacts.validator_config, + }, + description = "Reading validator-config.yaml from genesis artifacts", + ) artifacts_content = struct( nodes_yaml = nodes_yaml_result.output, validators_yaml = validators_yaml_result.output, config_yaml = config_yaml_result.output, + validator_config_yaml = validator_config_yaml_result.output, ) for i, service in enumerate(services): - # TODO: Support other client types - ream_launcher.start(plan, service, i, artifacts_content) + client_type = service.name.split("-")[0] + + # TODO: Add Qlean + if client_type == "ream": + launcher = ream_launcher + elif client_type == "zeam": + launcher = zeam_launcher + else: + plan.print("Unsupported client type during launch: {}".format(client_type)) + continue + launcher.start(plan, service, i, artifacts_content) \ No newline at end of file diff --git a/src/clients/zeam/zeam_launcher.star b/src/clients/zeam/zeam_launcher.star new file mode 100644 index 0000000..bb64c4e --- /dev/null +++ b/src/clients/zeam/zeam_launcher.star @@ -0,0 +1,114 @@ +""" +Module for launching a Zeam client. +""" + +common = import_module("../common.star") + +BASE_SERVICE_NAME = "zeam" +DEFAULT_IMAGE = "ethpandaops/zeam:latest" +ENTRYPOINT = "/app/zig-out/bin/zeam" + +def initialize(plan, image, index, key_artifact): + """ + Initialize a Zeam client with given image and index. + + Args: + plan: The plan object to execute actions. + image: The Docker image to use for the client. + index: The index of the participant. + key_artifact: The name of the files artifact containing the node key. + + Returns: + The launched service. + """ + + if image == "": + image = DEFAULT_IMAGE + + service_name = BASE_SERVICE_NAME + "-{}".format(index) + + # Zeam uses scratch base image, so we need to run zeam directly + # We'll start it with minimal config and configure it properly in start() + config = ServiceConfig( + image = image, + # Run zeam with a sleep loop to keep container alive until properly configured + cmd = ["sleep", "infinity"], + ports = { + "quic": PortSpec( + number = common.QUIC_PORT, + transport_protocol = "UDP", + wait = None, + ), + "http": PortSpec( + number = common.HTTP_PORT, + transport_protocol = "TCP", + wait = None, + ), + "metrics": PortSpec( + number = common.METRICS_PORT, + transport_protocol = "TCP", + wait = None, + ), + }, + files = { + "/config/keys": key_artifact, + }, + ) + return plan.add_service(service_name, config) + +def start(plan, service, node_index, artifacts_content): + """ + Start the Zeam client service with the provided genesis artifacts. + + Args: + plan: The plan object to execute actions. + service: The service object to start. + node_index: The index of this node. + artifacts_content: A struct containing the genesis artifact contents. + """ + + common.create_root_genesis_dir(plan, service) + common.copy_genesis_content( + plan, + service, + artifacts_content.nodes_yaml, + "/genesis/nodes.yaml", + ) + common.copy_genesis_content( + plan, + service, + artifacts_content.validators_yaml, + "/genesis/validators.yaml", + ) + common.copy_genesis_content( + plan, + service, + artifacts_content.config_yaml, + "/genesis/config.yaml", + ) + common.copy_genesis_content( + plan, + service, + artifacts_content.validator_config_yaml, + "/genesis/validator-config.yaml", + ) + + # Construct the full command as a single string + cmd_parts = [ + "--data-dir /data", + "--custom_genesis /genesis", + "--validator_config genesis_bootnode", + "--node-id " + service.name, + "--node-key /config/keys/node{}.key".format(node_index), + ] + full_cmd = " ".join(cmd_parts) + + log_file = common.get_log_file_path(service.name) + # TODO: Zeam supports logging to a file directly, use that + plan.exec( + service_name = service.name, + recipe = ExecRecipe( + command = ["nohup " + ENTRYPOINT + " " + full_cmd + " >> " + log_file + " 2>&1 &"], + ), + description = "Starting {}".format(service.name), + )