diff --git a/main.star b/main.star index 6dcd1b4a..d15923b7 100644 --- a/main.star +++ b/main.star @@ -8,7 +8,9 @@ op_challenger_launcher = import_module( "./src/challenger/op-challenger/op_challenger_launcher.star" ) +faucet = import_module("./src/faucet/op-faucet/op_faucet_launcher.star") observability = import_module("./src/observability/observability.star") +util = import_module("./src/util.star") wait_for_sync = import_module("./src/wait/wait_for_sync.star") input_parser = import_module("./src/package_io/input_parser.star") @@ -196,6 +198,16 @@ def run(plan, args={}): observability_helper=observability_helper, ) + if optimism_args.faucet.enabled: + _install_faucet( + plan=plan, + faucet_params=optimism_args.faucet, + l1_config_env_vars=l1_config_env_vars, + l1_priv_key=l1_priv_key, + deployment_output=deployment_output, + l2s=l2s, + ) + observability.launch( plan, observability_helper, global_node_selectors, observability_params ) @@ -211,3 +223,50 @@ def get_l1_config(all_l1_participants, l1_network_params, l1_network_id): env_vars["L1_CHAIN_ID"] = str(l1_network_id) env_vars["L1_BLOCK_TIME"] = str(l1_network_params.seconds_per_slot) return env_vars + + +def _install_faucet( + plan, + faucet_params, + l1_config_env_vars, + l1_priv_key, + deployment_output, + l2s, +): + faucets = [ + faucet.faucet_data( + name="l1", + chain_id=l1_config_env_vars["L1_CHAIN_ID"], + el_rpc=l1_config_env_vars["L1_RPC_URL"], + private_key=l1_priv_key, + ), + ] + for l2 in l2s: + chain_id = l2.network_id + + private_key = util.read_network_config_value( + plan, + deployment_output, + "wallets", + '."{0}" | .["l2FaucetPrivateKey"]'.format(chain_id), + ) + faucets.append( + faucet.faucet_data( + name=l2.name, + chain_id=chain_id, + el_rpc=l2.participants[0].el_context.rpc_http_url, + private_key=private_key, + ) + ) + + faucet_image = ( + faucet_params.image + if faucet_params.image != "" + else input_parser.DEFAULT_FAUCET_IMAGES["op-faucet"] + ) + faucet.launch( + plan, + "op-faucet", + faucet_image, + faucets, + ) diff --git a/src/faucet/op-faucet/config.tmpl b/src/faucet/op-faucet/config.tmpl new file mode 100644 index 00000000..384b4893 --- /dev/null +++ b/src/faucet/op-faucet/config.tmpl @@ -0,0 +1,8 @@ +faucets: + {{- range . }} + {{ .Name }}: + el_rpc: "{{ .RPC }}" + chain_id: {{ .ChainID }} + tx_cfg: + private_key: "{{ .PrivateKey }}" + {{ end }} diff --git a/src/faucet/op-faucet/op_faucet_launcher.star b/src/faucet/op-faucet/op_faucet_launcher.star new file mode 100644 index 00000000..ee76645e --- /dev/null +++ b/src/faucet/op-faucet/op_faucet_launcher.star @@ -0,0 +1,97 @@ +""" +Support for the op-faucet service. + +TODO: op-faucet doesn't have a proper release yet. Don't use this for now +unless you know what you're doing. +""" + + +def launch( + plan, + service_name, + image, + faucets, +): + """Launch the op-faucet service. + + Args: + plan: The plan to add the service to. + service_name (str): The name of the service. + image (str): The image to use for the op-faucet service. + faucets (list of faucet_data): The faucets to use for the op-faucet service. + """ + faucet_config = plan.render_templates( + name="faucet_config", + description="rendering op-faucet config", + config={ + "/config.yaml": struct( + template=read_file("./config.tmpl"), + data=faucets, + ) + }, + ) + + config = _get_config( + image, + faucet_config, + ) + plan.add_service(service_name, config) + + +def _get_config( + image, + faucet_config, +): + """Get the ServiceConfig for the op-faucet service. + + Args: + image (str): The image to use for the op-faucet service. + faucet_config (artifact): The config artifact for the op-faucet service. + """ + mount_path = "/config" + cmd = [ + "op-faucet", + "--rpc.port=9000", + "--config={0}/config.yaml".format(mount_path), + ] + + return ServiceConfig( + image=image, + cmd=cmd, + ports={ + "rpc": PortSpec( + number=9000, + transport_protocol="TCP", + application_protocol="http", + ), + }, + files={ + mount_path: faucet_config, + }, + ) + + +def faucet_data( + chain_id, + el_rpc, + private_key, + name=None, +): + """Constructor for a faucet data struct. + + Args: + chain_id (str): The chain ID the faucet will be used on. + el_rpc (str): The EL RPC the faucet will use. + private_key (str): The private key of the underlying faucet wallet. + name (str): The name of the faucet. + """ + if name == None: + name = chain_id + + return struct( + # capitalization for Go template expansion + Name=name, + ChainID=chain_id, + RPC=el_rpc, + PrivateKey=private_key, + ) diff --git a/src/package_io/input_parser.star b/src/package_io/input_parser.star index 7f2927ee..fdbe4af6 100644 --- a/src/package_io/input_parser.star +++ b/src/package_io/input_parser.star @@ -59,6 +59,12 @@ DEFAULT_TX_FUZZER_IMAGES = { "tx-fuzzer": "ethpandaops/tx-fuzz:master", } +DEFAULT_FAUCET_IMAGES = { + # TODO: update to use a versioned image when available + # For now, we'll need users to pass the image explicitly + "op-faucet": "", +} + DEFAULT_ADDITIONAL_SERVICES = [] @@ -120,6 +126,10 @@ def input_parser(plan, input_args): max_mem=results["observability"]["grafana_params"]["max_mem"], ), ), + faucet=struct( + enabled=results["faucet"]["enabled"], + image=results["faucet"]["image"], + ), interop=struct( enabled=results["interop"]["enabled"], supervisor_params=struct( @@ -308,6 +318,9 @@ def parse_network_params(plan, input_args): results["observability"] = default_observability_params() results["observability"].update(input_args.get("observability", {})) + results["faucet"] = default_faucet_params() + results["faucet"].update(input_args.get("faucet", {})) + results["observability"]["prometheus_params"] = default_prometheus_params() results["observability"]["prometheus_params"].update( input_args.get("observability", {}).get("prometheus_params", {}) @@ -482,6 +495,13 @@ def default_observability_params(): } +def default_faucet_params(): + return { + "enabled": False, + "image": DEFAULT_FAUCET_IMAGES["op-faucet"], + } + + def default_prometheus_params(): return { "image": "prom/prometheus:v3.1.0", diff --git a/src/package_io/sanity_check.star b/src/package_io/sanity_check.star index 2318047e..940cc758 100644 --- a/src/package_io/sanity_check.star +++ b/src/package_io/sanity_check.star @@ -8,6 +8,7 @@ ROOT_PARAMS = [ "global_node_selectors", "global_tolerations", "persistent", + "faucet", ] OBSERVABILITY_PARAMS = [ @@ -19,6 +20,11 @@ OBSERVABILITY_PARAMS = [ "grafana_params", ] +FAUCET_PARAMS = [ + "enabled", + "image", +] + PROMETHEUS_PARAMS = [ "image", "storage_tsdb_retention_time", @@ -256,6 +262,14 @@ def sanity_check(plan, optimism_config): GRAFANA_PARAMS, ) + if "faucet" in optimism_config: + validate_params( + plan, + optimism_config["faucet"], + "faucet", + FAUCET_PARAMS, + ) + if "interop" in optimism_config: validate_params( plan, diff --git a/src/participant_network.star b/src/participant_network.star index bd8abc51..f1e0ffe3 100644 --- a/src/participant_network.star +++ b/src/participant_network.star @@ -136,6 +136,7 @@ def launch_participant_network( ) return struct( + name=network_params.name, network_id=network_params.network_id, participants=all_participants, ) diff --git a/test/op_faucet_launcher_test.star b/test/op_faucet_launcher_test.star new file mode 100644 index 00000000..3bd6cc1c --- /dev/null +++ b/test/op_faucet_launcher_test.star @@ -0,0 +1,80 @@ +""" +Tests for the op-faucet launcher. +""" + +op_faucet_launcher = import_module("/src/faucet/op-faucet/op_faucet_launcher.star") +input_parser = import_module("/src/package_io/input_parser.star") +constants = import_module("/src/package_io/constants.star") + + +def test_launch_with_defaults(plan): + """Test launching the op-faucet service with default parameters.""" + faucet_image = input_parser.DEFAULT_FAUCET_IMAGES["op-faucet"] + service_name = "op-faucet" + + # Create test faucet data + faucets = [ + op_faucet_launcher.faucet_data( + chain_id="1", + el_rpc="http://l1-rpc", + private_key=constants.dev_accounts[0]["private_key"], + name="l1", + ), + op_faucet_launcher.faucet_data( + chain_id="10", + el_rpc="http://l2-rpc", + private_key=constants.dev_accounts[1]["private_key"], + name="l2", + ), + ] + + op_faucet_launcher.launch( + plan=plan, + service_name=service_name, + image=faucet_image, + faucets=faucets, + ) + + # Verify service configuration + faucet_service_config = kurtosistest.get_service_config(service_name=service_name) + expect.ne(faucet_service_config, None) + expect.eq(faucet_service_config.image, faucet_image) + expect.eq(faucet_service_config.env_vars, {}) + expect.eq(faucet_service_config.entrypoint, []) + expect.eq( + faucet_service_config.cmd, + [ + "op-faucet", + "--rpc.port=9000", + "--config=/config/config.yaml", + ], + ) + expect.eq(faucet_service_config.ports["rpc"].number, 9000) + expect.eq(faucet_service_config.ports["rpc"].transport_protocol, "TCP") + expect.eq(faucet_service_config.ports["rpc"].application_protocol, "http") + + +def test_launch_with_custom_image(plan): + """Test launching the op-faucet service with a custom image.""" + custom_image = "custom-op-faucet:latest" + service_name = "op-faucet" + + # Create test faucet data + faucets = [ + op_faucet_launcher.faucet_data( + chain_id="1", + el_rpc="http://l1-rpc", + private_key=constants.dev_accounts[0]["private_key"], + ), + ] + + op_faucet_launcher.launch( + plan=plan, + service_name=service_name, + image=custom_image, + faucets=faucets, + ) + + # Verify service configuration + faucet_service_config = kurtosistest.get_service_config(service_name=service_name) + expect.eq(faucet_service_config.image, custom_image)