From 5031d94712c30d76967e4b4b830659b388475c08 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 14 Apr 2025 12:57:15 -0700 Subject: [PATCH 01/14] chore: Proposal for multiple supervisors --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cb22ec8..f1e19c2f 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,18 @@ optimism_package: image: "grafana/grafana:11.5.0" # Interop configuration interop: - # Whether or not to enable interop mode - enabled: false + # A list of interop sets - connected L2 chains + # + # If left empty, interop is disabled + sets: + # Optional human-readable name for this interop set + - name: + # List of L2 IDs that participate in this set + participants: [] + # Supervisor overrides for this particular interop set + # + # Leave empty to use the default supervisor configuration + supervisor_params: # Default supervisor configuration supervisor_params: # The Docker image that should be used for the supervisor; leave blank to use the default op-supervisor image From 5480d26dd53062140c7dca76aac9e81b62c67b85 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 15 Apr 2025 10:04:15 -0700 Subject: [PATCH 02/14] chore: Add filter_none helper function for removing None values from dictionaries --- src/util.star | 4 ++++ test/util_test.star | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/util.star b/src/util.star index 336cb6b5..8bf7ea75 100644 --- a/src/util.star +++ b/src/util.star @@ -144,3 +144,7 @@ def make_execution_rpc_url(el_context): el_context.ip_addr, el_context.rpc_port_num, ) + +# Removes all None values from a dictionary and returns a new dictionary. +def filter_none(d): + return {k: v for k, v in d.items() if v != None} diff --git a/test/util_test.star b/test/util_test.star index 9f9a7caa..db26429f 100644 --- a/test/util_test.star +++ b/test/util_test.star @@ -58,3 +58,13 @@ def test_label_from_image_long_image_name_long_suffix(plan): image_label = util.label_from_image(image_name) expect.eq(len(image_label), 63) expect.eq(image_suffix[-63:], image_label) + +def test_filter_none(plan): + expect.eq(util.filter_none({}), {}) + expect.eq(util.filter_none({ "key": "value" }), { "key": "value" }) + expect.eq(util.filter_none({ "key": None }), {}) + expect.eq(util.filter_none({ "key": False }), { "key": False }) + expect.eq(util.filter_none({ "key": 0 }), { "key": 0 }) + expect.eq(util.filter_none({ "key": "" }), { "key": "" }) + expect.eq(util.filter_none({ "key": [] }), { "key": [] }) + expect.eq(util.filter_none({ "key": {} }), { "key": {} }) From 7e1dcfd6d207f96768962e2d4c7533ce841a8ef8 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 15 Apr 2025 12:14:40 -0700 Subject: [PATCH 03/14] wip: Interop args update --- src/package_io/input_parser.star | 91 +++++++++++++++++++++----- test/package_io/input_parser_test.star | 68 +++++++++++++++++++ 2 files changed, 141 insertions(+), 18 deletions(-) diff --git a/src/package_io/input_parser.star b/src/package_io/input_parser.star index 8b3b63bd..0d01d5ce 100644 --- a/src/package_io/input_parser.star +++ b/src/package_io/input_parser.star @@ -2,8 +2,9 @@ ethereum_package_input_parser = import_module( "github.com/ethpandaops/ethereum-package/src/package_io/input_parser.star" ) -constants = import_module("../package_io/constants.star") +constants = import_module("./constants.star") sanity_check = import_module("./sanity_check.star") +util = import_module("../util.star") DEFAULT_EL_IMAGES = { "op-geth": "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:latest", @@ -314,13 +315,7 @@ def parse_network_params(plan, input_args): # configure interop - results["interop"] = default_interop_params() - results["interop"].update(input_args.get("interop", {})) - - results["interop"]["supervisor_params"] = default_supervisor_params() - results["interop"]["supervisor_params"].update( - input_args.get("interop", {}).get("supervisor_params", {}) - ) + results["interop"] = build_interop_params(input_args.get("interop", {})) # configure altda @@ -516,10 +511,78 @@ def default_promtail_params(): "max_mem": 2048, } - def default_interop_params(): return { - "enabled": False, + # Interop sets organize the networks into disconnected interop chain sets + # + # If there are no sets defined, interop is effectively disabled. + "sets": [], + # Default values to apply for all interop sets' supervisors + "supervisor_params": default_supervisor_params(), + } + +def default_supervisor_params(): + return { + "image": DEFAULT_SUPERVISOR_IMAGES["op-supervisor"], + "dependency_set": "", + "extra_params": [], + } + +# This function normalizes the interop args to ensure that all values are set +def build_interop_params(interop_args): + interop_args_without_none = util.filter_none(interop_args) + supervisor_params = build_interop_supervisor_params(interop_args_without_none.get("supervisor_params", {})) + sets_params = build_interop_sets_params(interop_args_without_none.get("sets", []), supervisor_params) + + return { + "sets": sets_params, + # This value is potentially no longer necessary here as all the interop sets + # have their supervisor params set + "supervisor_params": supervisor_params, + } + +# This function normalizes the interop supervisor_params args to ensure that all values are set +# +# It is being used in two places: +# +# - to build the default interop supervisor params, in which case the default values are the global defaults +# - to build the interop supervisor params for each interop set, in which case the default values are the interop supervisor params +def build_interop_supervisor_params( + interop_supervisor_args, + default_interop_supervisor_params = default_supervisor_params() +): + interop_supervisor_args_without_none = util.filter_none(interop_supervisor_args) + + return { + "image": interop_supervisor_args_without_none.get("image", default_interop_supervisor_params["image"]), + "dependency_set": interop_supervisor_args_without_none.get("dependency_set", default_interop_supervisor_params["dependency_set"]), + "extra_params": interop_supervisor_args_without_none.get("extra_params", default_interop_supervisor_params["extra_params"]), + } + +# This function normalizes the interop sets args to ensure that all values are set +def build_interop_sets_params(interop_sets_args, interop_supervisor_params): + return [ + build_interop_set_params( + interop_set_args, + interop_set_index, + interop_supervisor_params + ) + for interop_set_index, interop_set_args in enumerate(interop_sets_args) if interop_set_args != None + ] + +# This function normalizes the interop sets args to ensure that all values are set +def build_interop_set_params( + interop_set_args, + # The suffix is used to create a default name for the interop set if no name is provided + interop_set_suffix, + interop_supervisor_params +): + interop_set_args_without_none = util.filter_none(interop_set_args) + + return { + "name": interop_set_args_without_none.get("name", "interop-set-{}".format(interop_set_suffix)), + "participants": interop_set_args_without_none.get("participants", []), + "supervisor_params": build_interop_supervisor_params(interop_set_args_without_none.get("supervisor_params", {}), interop_supervisor_params) } @@ -534,14 +597,6 @@ def default_altda_deploy_config(): } -def default_supervisor_params(): - return { - "image": DEFAULT_SUPERVISOR_IMAGES["op-supervisor"], - "dependency_set": "", - "extra_params": [], - } - - def default_mev_params(): return { "rollup_boost_image": "", diff --git a/test/package_io/input_parser_test.star b/test/package_io/input_parser_test.star index 5bc22d2a..6a3d3ffb 100644 --- a/test/package_io/input_parser_test.star +++ b/test/package_io/input_parser_test.star @@ -90,3 +90,71 @@ def test_external_l1_network_params_input_parser_set_fields(plan): parsed_params = input_parser.external_l1_network_params_input_parser(plan, params) expect.eq(parsed_params, struct(**params)) + +def test_interop_default_args(plan): + parsed_params = input_parser.parse_network_params(plan, {}) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params()) + +def test_interop_supervisor_params(plan): + supervisor_args = { "image": "supervisor.jpeg", "dependency_set": None, "extra_params": None } + parsed_params = input_parser.parse_network_params(plan, { + "interop": { + "supervisor_params": supervisor_args, + }, + }) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "supervisor_params": { + "image": "supervisor.jpeg", + "dependency_set": "", + "extra_params": [], + }, + }) + +def test_interop_default_set_params(plan): + supervisor_args = { "image": "supervisor.jpeg" } + parsed_params = input_parser.parse_network_params(plan, { + "interop": { + "supervisor_params": supervisor_args, + "sets": [{}] + }, + }) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "supervisor_params": input_parser.default_supervisor_params() | supervisor_args, + "sets": [{ + "participants": [], + "name": "interop-set-0", + "supervisor_params": input_parser.default_supervisor_params() | supervisor_args + }] + }) + +def test_interop_set_params(plan): + supervisor_args = { "image": "supervisor.jpeg" } + parsed_params = input_parser.parse_network_params(plan, { + "interop": { + "supervisor_params": supervisor_args, + "sets": [{}] + }, + }) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "supervisor_params": input_parser.default_supervisor_params() | supervisor_args, + "sets": [{ + "participants": [], + "name": "interop-set-0", + "supervisor_params": input_parser.default_supervisor_params() | supervisor_args + }] + }) + +def test_interop_set_none_params(plan): + parsed_params = input_parser.parse_network_params(plan, { + "interop": { + "sets": [None] + }, + }) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "sets": [] + }) \ No newline at end of file From d26d2c329f3b309158088270e4671953816cf08f Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 15 Apr 2025 13:26:34 -0700 Subject: [PATCH 04/14] chore: get_duplicates helper --- src/util.star | 14 ++++++++++++++ test/util_test.star | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/util.star b/src/util.star index 8bf7ea75..5f6ef482 100644 --- a/src/util.star +++ b/src/util.star @@ -148,3 +148,17 @@ def make_execution_rpc_url(el_context): # Removes all None values from a dictionary and returns a new dictionary. def filter_none(d): return {k: v for k, v in d.items() if v != None} + +# Returns a list of duplicate items in the input list. +def get_duplicates(items): + # Unfortunately kurotis star doesn't support sets so we'll have to do with O(N^2) complexity and a list + # + # The redeeming factor is the fact that we are dealing with very limited list sizes + seen = [] + duplicates = [] + for item in items: + if item in seen: + if item not in duplicates: duplicates.append(item) + else: + seen.append(item) + return duplicates \ No newline at end of file diff --git a/test/util_test.star b/test/util_test.star index db26429f..c8e7b321 100644 --- a/test/util_test.star +++ b/test/util_test.star @@ -68,3 +68,13 @@ def test_filter_none(plan): expect.eq(util.filter_none({ "key": "" }), { "key": "" }) expect.eq(util.filter_none({ "key": [] }), { "key": [] }) expect.eq(util.filter_none({ "key": {} }), { "key": {} }) + +def test_get_duplicates(plan): + expect.eq(util.get_duplicates([]), []) + expect.eq(util.get_duplicates([1]), []) + expect.eq(util.get_duplicates([1, 1]), [1]) + expect.eq(util.get_duplicates([1, 1, 1, 2, 2, 2, 3]), [1, 2]) + expect.eq(util.get_duplicates([1, 2, "1"]), []) + expect.eq(util.get_duplicates([{}, {}]), [{}]) + expect.eq(util.get_duplicates([{}, {"key": "value"}]), []) + expect.eq(util.get_duplicates([[1], [1]]), [[1]]) From 47cb6c83fed6b3b97b03aa907c3e53a5bc662543 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 15 Apr 2025 13:39:02 -0700 Subject: [PATCH 05/14] chore: Update docs --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f1e19c2f..d2571856 100644 --- a/README.md +++ b/README.md @@ -138,13 +138,18 @@ optimism_package: # If left empty, interop is disabled sets: # Optional human-readable name for this interop set - - name: - # List of L2 IDs that participate in this set - participants: [] + - name: "interop-set-0" + # List of L2 network_ids that participate in this set + # + # Please refer to chains[].network_params.network_id for more information + participants: ["2151908"] # Supervisor overrides for this particular interop set # # Leave empty to use the default supervisor configuration supervisor_params: + - name: "interop-set-1" + # "*" can be used to quickly add all networks into one interop set + participants: "*" # Default supervisor configuration supervisor_params: # The Docker image that should be used for the supervisor; leave blank to use the default op-supervisor image From 23dbea0313adabb32404db90692d034a669cd590 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 15 Apr 2025 13:39:11 -0700 Subject: [PATCH 06/14] chore: Update input parser --- src/package_io/input_parser.star | 71 +++++++++++--- test/package_io/input_parser_test.star | 131 ++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 15 deletions(-) diff --git a/src/package_io/input_parser.star b/src/package_io/input_parser.star index 0d01d5ce..a11c2f90 100644 --- a/src/package_io/input_parser.star +++ b/src/package_io/input_parser.star @@ -313,10 +313,6 @@ def parse_network_params(plan, input_args): input_args.get("observability", {}).get("grafana_params", {}) ) - # configure interop - - results["interop"] = build_interop_params(input_args.get("interop", {})) - # configure altda results["altda_deploy_config"] = default_altda_deploy_config() @@ -438,6 +434,10 @@ def parse_network_params(plan, input_args): results["chains"] = chains + # configure interop + + results["interop"] = build_interop_params(input_args.get("interop", {}), chains) + # configure op-deployer results["op_contract_deployer_params"] = default_op_contract_deployer_params() @@ -513,6 +513,7 @@ def default_promtail_params(): def default_interop_params(): return { + "enabled": False, # Interop sets organize the networks into disconnected interop chain sets # # If there are no sets defined, interop is effectively disabled. @@ -529,12 +530,19 @@ def default_supervisor_params(): } # This function normalizes the interop args to ensure that all values are set -def build_interop_params(interop_args): +def build_interop_params(interop_args, chains): + # We first filter the None values so that we can merge dicts easily interop_args_without_none = util.filter_none(interop_args) + + # Then we build the sub-params supervisor_params = build_interop_supervisor_params(interop_args_without_none.get("supervisor_params", {})) - sets_params = build_interop_sets_params(interop_args_without_none.get("sets", []), supervisor_params) + sets_params = build_interop_sets_params(interop_args_without_none.get("sets", []), supervisor_params, chains) + + # If enabled is not explicitly set, we enable interop if there are any sets defined + enabled_param = interop_args_without_none.get("enabled", len(sets_params) > 0) return { + "enabled": enabled_param, "sets": sets_params, # This value is potentially no longer necessary here as all the interop sets # have their supervisor params set @@ -560,31 +568,68 @@ def build_interop_supervisor_params( } # This function normalizes the interop sets args to ensure that all values are set -def build_interop_sets_params(interop_sets_args, interop_supervisor_params): - return [ +def build_interop_sets_params(interop_sets_args, interop_supervisor_params, chains): + interop_sets_params = [ build_interop_set_params( interop_set_args, interop_set_index, - interop_supervisor_params + interop_supervisor_params, + chains ) for interop_set_index, interop_set_args in enumerate(interop_sets_args) if interop_set_args != None ] + return interop_sets_params + # This function normalizes the interop sets args to ensure that all values are set def build_interop_set_params( interop_set_args, # The suffix is used to create a default name for the interop set if no name is provided interop_set_suffix, - interop_supervisor_params + interop_supervisor_params, + chains ): interop_set_args_without_none = util.filter_none(interop_set_args) + # Iterop set name is optional + interop_set_name = interop_set_args_without_none.get("name", "interop-set-{}".format(interop_set_suffix)) + + # Interop set participants can be specified as "*" to include all networks (default) + # or as a list of network ids + interop_set_participants = interop_set_args_without_none.get("participants", "*") + expanded_interop_set_participants = expand_interop_set_participants(interop_set_participants, chains) + + # The interop set supervisor params are optional and default to the global interop supervisor params + interop_set_supervisor_params = build_interop_supervisor_params(interop_set_args_without_none.get("supervisor_params", {}), interop_supervisor_params) + return { - "name": interop_set_args_without_none.get("name", "interop-set-{}".format(interop_set_suffix)), - "participants": interop_set_args_without_none.get("participants", []), - "supervisor_params": build_interop_supervisor_params(interop_set_args_without_none.get("supervisor_params", {}), interop_supervisor_params) + "name": interop_set_name, + "participants": expanded_interop_set_participants, + "supervisor_params": interop_set_supervisor_params, } +def expand_interop_set_participants(interop_set_participants, chains): + # kurtosis starlark doesn't support sets so we'll use a hashmap instead + all_network_ids = {chain["network_params"]["network_id"]: True for chain in chains} + + # "*" is used as a shortcut to include all networks + if interop_set_participants == "*": + return all_network_ids.keys() + elif type(interop_set_participants) == "list": + # First we check that all the network IDs exist + network_ids = [ + network_id if all_network_ids.get(network_id) else fail("Unknown network id in list of interop participants: {}".format(network_id)) for network_id in interop_set_participants + ] + + # Then we make sure that there are no duplicates within one interop set + duplicate_network_ids = util.get_duplicates(network_ids) + if len(duplicate_network_ids) > 0: + fail("Duplicate network ids in list of interop participants: {}".format(duplicate_network_ids)) + + return network_ids + else: + fail("Invalid interop set participants: {}".format(interop_set_participants)) + def default_altda_deploy_config(): return { diff --git a/test/package_io/input_parser_test.star b/test/package_io/input_parser_test.star index 6a3d3ffb..b8c19356 100644 --- a/test/package_io/input_parser_test.star +++ b/test/package_io/input_parser_test.star @@ -122,9 +122,10 @@ def test_interop_default_set_params(plan): }) expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "enabled": True, "supervisor_params": input_parser.default_supervisor_params() | supervisor_args, "sets": [{ - "participants": [], + "participants": ["2151908"], "name": "interop-set-0", "supervisor_params": input_parser.default_supervisor_params() | supervisor_args }] @@ -140,14 +141,123 @@ def test_interop_set_params(plan): }) expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "enabled": True, "supervisor_params": input_parser.default_supervisor_params() | supervisor_args, "sets": [{ - "participants": [], + "participants": ["2151908"], "name": "interop-set-0", "supervisor_params": input_parser.default_supervisor_params() | supervisor_args }] }) +def test_interop_set_all_participants_by_default(plan): + parsed_params = input_parser.parse_network_params(plan, { + "chains": [{ + "network_params": { + "network_id": "network-0", + }, + }, { + "network_params": { + "network_id": "network-1", + }, + }], + "interop": { + "sets": [{}] + }, + }) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "enabled": True, + "sets": [{ + "name": "interop-set-0", + "participants": ["network-0", "network-1"], + "supervisor_params": input_parser.default_supervisor_params() + }] + }) + +def test_interop_set_all_participants_explicitly(plan): + parsed_params = input_parser.parse_network_params(plan, { + "chains": [{ + "network_params": { + "network_id": "network-0", + }, + }, { + "network_params": { + "network_id": "network-1", + }, + }], + "interop": { + "sets": [{ + "participants": "*" + }] + }, + }) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "enabled": True, + "sets": [{ + "name": "interop-set-0", + "participants": ["network-0", "network-1"], + "supervisor_params": input_parser.default_supervisor_params() + }] + }) + +# This test tests an invalid configuration that we want to support to model misconfiguration +# +# In this test case we have two interop sets that both contain the same network +def test_interop_set_all_participants_multiple_times(plan): + parsed_params = input_parser.parse_network_params(plan, { + "chains": [{ + "network_params": { + "network_id": "network-0", + }, + }], + "interop": { + "sets": [{}, {}] + }, + }) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "enabled": True, + "sets": [{ + "name": "interop-set-0", + "participants": ["network-0"], + "supervisor_params": input_parser.default_supervisor_params(), + }, { + "name": "interop-set-1", + "participants": ["network-0"], + "supervisor_params": input_parser.default_supervisor_params(), + }] + }) + +def test_interop_set_duplicate_participants(plan): + expect.fails(lambda: input_parser.parse_network_params(plan, { + "chains": [{ + "network_params": { + "network_id": "network-0", + }, + }], + "interop": { + "sets": [{ + "participants": ["network-0", "network-0"], + }] + }, + }), "Duplicate network ids in list of interop participants: \\[\"network-0\"\\]") + +def test_interop_set_nonexistent_participants(plan): + expect.fails(lambda: input_parser.parse_network_params(plan, { + "chains": [{ + "network_params": { + "network_id": "network-0", + }, + }], + "interop": { + "sets": [{ + "participants": ["network-1"], + }] + }, + }), "Unknown network id in list of interop participants: network-1") + def test_interop_set_none_params(plan): parsed_params = input_parser.parse_network_params(plan, { "interop": { @@ -157,4 +267,21 @@ def test_interop_set_none_params(plan): expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { "sets": [] + }) + +def test_interop_explicitly_disabled(plan): + parsed_params = input_parser.parse_network_params(plan, { + "interop": { + "enabled": False, + "sets": [{}] + }, + }) + + expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { + "enabled": False, + "sets": [{ + "name": "interop-set-0", + "participants": ["2151908"], + "supervisor_params": input_parser.default_supervisor_params() + }] }) \ No newline at end of file From 3ac16d2408ae3931ebfcb76dd64bf97075dd82b3 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 21 Apr 2025 19:50:32 -0700 Subject: [PATCH 07/14] feat: Multiple supervisors --- main.star | 18 +- mise.toml | 2 +- src/interop/constants.star | 1 - .../op-supervisor/op_supervisor_launcher.star | 66 ++- src/l2.star | 2 + src/package_io/input_parser.star | 110 ++++- src/package_io/sanity_check.star | 5 +- src/participant_network.star | 1 + src/util.star | 9 +- .../op_supervisor_launcher_test.star | 259 ++++++++++ test/package_io/input_parser_test.star | 464 ++++++++++++------ test/util_test.star | 16 +- 12 files changed, 734 insertions(+), 219 deletions(-) create mode 100644 test/interop/op-supervisor/op_supervisor_launcher_test.star diff --git a/main.star b/main.star index b74ace39..1dd41f61 100644 --- a/main.star +++ b/main.star @@ -124,15 +124,15 @@ def run(plan, args={}): ) if interop_params.enabled: - op_supervisor_launcher.launch( - plan, - l1_config_env_vars, - optimism_args.chains, - l2s, - jwt_file, - interop_params.supervisor_params, - observability_helper, - ) + for i, interop_set in enumerate(interop_params.sets): + op_supervisor_launcher.launch( + plan=plan, + interop_set=interop_set, + l1_config_env_vars=l1_config_env_vars, + l2s=l2s, + jwt_file=jwt_file, + observability_helper=observability_helper, + ) # challenger must launch after supervisor because it depends on it for interop for l2_num, l2 in enumerate(l2s): diff --git a/mise.toml b/mise.toml index 8af3abf8..d4161bbb 100644 --- a/mise.toml +++ b/mise.toml @@ -1,7 +1,7 @@ [tools] # Core dependencies -"ubi:ethereum-optimism/kurtosis-test" = "0.0.1" +"ubi:ethereum-optimism/kurtosis-test" = "0.0.3" "ubi:kurtosis-tech/kurtosis-cli-release-artifacts[exe=kurtosis]" = "1.4.4" "yq" = "v4.44.3" diff --git a/src/interop/constants.star b/src/interop/constants.star index 023b686b..8f836e1e 100644 --- a/src/interop/constants.star +++ b/src/interop/constants.star @@ -1,4 +1,3 @@ -constants = import_module("../package_io/constants.star") util = import_module("../util.star") INTEROP_WS_PORT_ID = "interop-ws" diff --git a/src/interop/op-supervisor/op_supervisor_launcher.star b/src/interop/op-supervisor/op_supervisor_launcher.star index 6f462cce..fa205153 100644 --- a/src/interop/op-supervisor/op_supervisor_launcher.star +++ b/src/interop/op-supervisor/op_supervisor_launcher.star @@ -8,10 +8,10 @@ ethereum_package_constants = import_module( "github.com/ethpandaops/ethereum-package/src/package_io/constants.star" ) -constants = import_module("../../package_io/constants.star") observability = import_module("../../observability/observability.star") prometheus = import_module("../../observability/prometheus/prometheus_launcher.star") +constants = import_module("../../package_io/constants.star") interop_constants = import_module("../constants.star") @@ -30,15 +30,15 @@ DATA_DIR = "/etc/op-supervisor" DEPENDENCY_SET_FILE_NAME = "dependency_set.json" -def create_dependency_set(chains): +def create_dependency_set(l2s): result = { "dependencies": { - str(chain.network_params.network_id): { - "chainIndex": str(chain.network_params.network_id), + str(l2.network_id): { + "chainIndex": str(l2.network_id), "activationTime": 0, "historyMinTime": 0, } - for chain in chains + for l2 in l2s } } return result @@ -46,40 +46,66 @@ def create_dependency_set(chains): def launch( plan, + interop_set, l1_config_env_vars, - chains, l2s, jwt_file, - supervisor_params, observability_helper, ): - dependency_set_json = supervisor_params.dependency_set - if not dependency_set_json: - dependency_set = create_dependency_set(chains) - dependency_set_json = json.encode(dependency_set) + # First we check that the supervisor is enabled for this interop set + if not interop_set.enabled: + plan.print( + "op-supervisor is not enabled for interop set {}, skipping launch".format( + interop_set.name + ) + ) + return None + + # Then we check that we have some participants + if len(interop_set.participants) == 0: + plan.print( + "op-supervisor has no participants for interop set {}, skipping launch".format( + interop_set.name + ) + ) + return None + + # Now we filter out the participating L2s + interop_set_l2s = [l2 for l2 in l2s if l2.network_id in interop_set.participants] + + # Now we create dependency set if none was provided + dependency_set_json = interop_set.supervisor_params.dependency_set or json.encode( + create_dependency_set(interop_set_l2s) + ) + # And write it to an artifact dependency_set_artifact = utils.write_to_file( plan, dependency_set_json, DATA_DIR, DEPENDENCY_SET_FILE_NAME ) + # We create a service name based on the interop set name + service_name = "{}-{}".format( + interop_constants.SUPERVISOR_SERVICE_NAME, interop_set.name + ) + config = get_supervisor_config( - plan, - l1_config_env_vars, - l2s, - jwt_file, - dependency_set_artifact, - supervisor_params, - observability_helper, + plan=plan, + l1_config_env_vars=l1_config_env_vars, + l2s=interop_set_l2s, + jwt_file=jwt_file, + dependency_set_artifact=dependency_set_artifact, + supervisor_params=interop_set.supervisor_params, + observability_helper=observability_helper, ) - service = plan.add_service(interop_constants.SUPERVISOR_SERVICE_NAME, config) + service = plan.add_service(service_name, config) observability.register_op_service_metrics_job( observability_helper, service, ) - return "op_supervisor" + return service def get_supervisor_config( diff --git a/src/l2.star b/src/l2.star index c5cd85a8..eefc0e99 100644 --- a/src/l2.star +++ b/src/l2.star @@ -48,6 +48,8 @@ def launch_l2( ) plan.print("Successfully launched da-server") + kurtosistest.debug(l2_num) + l2 = participant_network.launch_participant_network( plan, l2_args.participants, diff --git a/src/package_io/input_parser.star b/src/package_io/input_parser.star index a11c2f90..caba4c0a 100644 --- a/src/package_io/input_parser.star +++ b/src/package_io/input_parser.star @@ -129,6 +129,21 @@ def input_parser(plan, input_args): ], extra_params=results["interop"]["supervisor_params"]["extra_params"], ), + sets=[ + struct( + enabled=interop_set["enabled"], + name=interop_set["name"], + participants=interop_set["participants"], + supervisor_params=struct( + image=interop_set["supervisor_params"]["image"], + dependency_set=interop_set["supervisor_params"][ + "dependency_set" + ], + extra_params=interop_set["supervisor_params"]["extra_params"], + ), + ) + for interop_set in results["interop"]["sets"] + ], ), altda_deploy_config=struct( use_altda=results["altda_deploy_config"]["use_altda"], @@ -511,17 +526,19 @@ def default_promtail_params(): "max_mem": 2048, } + def default_interop_params(): return { "enabled": False, # Interop sets organize the networks into disconnected interop chain sets - # + # # If there are no sets defined, interop is effectively disabled. "sets": [], # Default values to apply for all interop sets' supervisors "supervisor_params": default_supervisor_params(), } + def default_supervisor_params(): return { "image": DEFAULT_SUPERVISOR_IMAGES["op-supervisor"], @@ -529,15 +546,20 @@ def default_supervisor_params(): "extra_params": [], } + # This function normalizes the interop args to ensure that all values are set def build_interop_params(interop_args, chains): # We first filter the None values so that we can merge dicts easily interop_args_without_none = util.filter_none(interop_args) # Then we build the sub-params - supervisor_params = build_interop_supervisor_params(interop_args_without_none.get("supervisor_params", {})) - sets_params = build_interop_sets_params(interop_args_without_none.get("sets", []), supervisor_params, chains) - + supervisor_params = build_interop_supervisor_params( + interop_args_without_none.get("supervisor_params", {}) + ) + sets_params = build_interop_sets_params( + interop_args_without_none.get("sets", []), supervisor_params, chains + ) + # If enabled is not explicitly set, we enable interop if there are any sets defined enabled_param = interop_args_without_none.get("enabled", len(sets_params) > 0) @@ -549,65 +571,96 @@ def build_interop_params(interop_args, chains): "supervisor_params": supervisor_params, } + # This function normalizes the interop supervisor_params args to ensure that all values are set -# +# # It is being used in two places: -# +# # - to build the default interop supervisor params, in which case the default values are the global defaults # - to build the interop supervisor params for each interop set, in which case the default values are the interop supervisor params def build_interop_supervisor_params( interop_supervisor_args, - default_interop_supervisor_params = default_supervisor_params() + default_interop_supervisor_params=default_supervisor_params(), ): interop_supervisor_args_without_none = util.filter_none(interop_supervisor_args) return { - "image": interop_supervisor_args_without_none.get("image", default_interop_supervisor_params["image"]), - "dependency_set": interop_supervisor_args_without_none.get("dependency_set", default_interop_supervisor_params["dependency_set"]), - "extra_params": interop_supervisor_args_without_none.get("extra_params", default_interop_supervisor_params["extra_params"]), + "image": interop_supervisor_args_without_none.get( + "image", default_interop_supervisor_params["image"] + ), + "dependency_set": interop_supervisor_args_without_none.get( + "dependency_set", default_interop_supervisor_params["dependency_set"] + ), + "extra_params": interop_supervisor_args_without_none.get( + "extra_params", default_interop_supervisor_params["extra_params"] + ), } + # This function normalizes the interop sets args to ensure that all values are set def build_interop_sets_params(interop_sets_args, interop_supervisor_params, chains): - interop_sets_params = [ + interop_sets_params = [ build_interop_set_params( - interop_set_args, - interop_set_index, - interop_supervisor_params, - chains + interop_set_args, interop_set_index, interop_supervisor_params, chains ) - for interop_set_index, interop_set_args in enumerate(interop_sets_args) if interop_set_args != None + for interop_set_index, interop_set_args in enumerate(interop_sets_args) + if interop_set_args != None ] + # We need to make sure the interop set names are unique + interop_set_names = [interop_set["name"] for interop_set in interop_sets_params] + duplicate_interop_set_names = util.get_duplicates(interop_set_names) + if len(duplicate_interop_set_names) > 0: + fail( + "Duplicate interop set names: {}".format( + ",".join(duplicate_interop_set_names) + ) + ) + return interop_sets_params + # This function normalizes the interop sets args to ensure that all values are set def build_interop_set_params( interop_set_args, # The suffix is used to create a default name for the interop set if no name is provided interop_set_suffix, interop_supervisor_params, - chains + chains, ): interop_set_args_without_none = util.filter_none(interop_set_args) # Iterop set name is optional - interop_set_name = interop_set_args_without_none.get("name", "interop-set-{}".format(interop_set_suffix)) - + interop_set_name = interop_set_args_without_none.get( + "name", "interop-set-{}".format(interop_set_suffix) + ) + # Interop set participants can be specified as "*" to include all networks (default) # or as a list of network ids interop_set_participants = interop_set_args_without_none.get("participants", "*") - expanded_interop_set_participants = expand_interop_set_participants(interop_set_participants, chains) + expanded_interop_set_participants = expand_interop_set_participants( + interop_set_participants, chains + ) + + # If enabled is not explicitly set, we enable interop if there are any participants defined + interop_set_enabled = interop_set_args_without_none.get( + "enabled", len(expanded_interop_set_participants) > 0 + ) # The interop set supervisor params are optional and default to the global interop supervisor params - interop_set_supervisor_params = build_interop_supervisor_params(interop_set_args_without_none.get("supervisor_params", {}), interop_supervisor_params) + interop_set_supervisor_params = build_interop_supervisor_params( + interop_set_args_without_none.get("supervisor_params", {}), + interop_supervisor_params, + ) return { + "enabled": interop_set_enabled, "name": interop_set_name, "participants": expanded_interop_set_participants, "supervisor_params": interop_set_supervisor_params, } + def expand_interop_set_participants(interop_set_participants, chains): # kurtosis starlark doesn't support sets so we'll use a hashmap instead all_network_ids = {chain["network_params"]["network_id"]: True for chain in chains} @@ -618,13 +671,24 @@ def expand_interop_set_participants(interop_set_participants, chains): elif type(interop_set_participants) == "list": # First we check that all the network IDs exist network_ids = [ - network_id if all_network_ids.get(network_id) else fail("Unknown network id in list of interop participants: {}".format(network_id)) for network_id in interop_set_participants + network_id + if all_network_ids.get(network_id) + else fail( + "Unknown network id in list of interop participants: {}".format( + network_id + ) + ) + for network_id in interop_set_participants ] # Then we make sure that there are no duplicates within one interop set duplicate_network_ids = util.get_duplicates(network_ids) if len(duplicate_network_ids) > 0: - fail("Duplicate network ids in list of interop participants: {}".format(duplicate_network_ids)) + fail( + "Duplicate network ids in list of interop participants: {}".format( + ",".join(duplicate_network_ids) + ) + ) return network_ids else: diff --git a/src/package_io/sanity_check.star b/src/package_io/sanity_check.star index ddbc60e0..2318047e 100644 --- a/src/package_io/sanity_check.star +++ b/src/package_io/sanity_check.star @@ -54,10 +54,7 @@ GRAFANA_PARAMS = [ "max_mem", ] -INTEROP_PARAMS = [ - "enabled", - "supervisor_params", -] +INTEROP_PARAMS = ["enabled", "supervisor_params", "sets"] SUPERVISOR_PARAMS = [ "image", diff --git a/src/participant_network.star b/src/participant_network.star index b9bc8f80..bd8abc51 100644 --- a/src/participant_network.star +++ b/src/participant_network.star @@ -136,5 +136,6 @@ def launch_participant_network( ) return struct( + network_id=network_params.network_id, participants=all_participants, ) diff --git a/src/util.star b/src/util.star index 5f6ef482..0e282583 100644 --- a/src/util.star +++ b/src/util.star @@ -145,20 +145,23 @@ def make_execution_rpc_url(el_context): el_context.rpc_port_num, ) + # Removes all None values from a dictionary and returns a new dictionary. def filter_none(d): return {k: v for k, v in d.items() if v != None} + # Returns a list of duplicate items in the input list. def get_duplicates(items): # Unfortunately kurotis star doesn't support sets so we'll have to do with O(N^2) complexity and a list - # + # # The redeeming factor is the fact that we are dealing with very limited list sizes seen = [] duplicates = [] for item in items: if item in seen: - if item not in duplicates: duplicates.append(item) + if item not in duplicates: + duplicates.append(item) else: seen.append(item) - return duplicates \ No newline at end of file + return duplicates diff --git a/test/interop/op-supervisor/op_supervisor_launcher_test.star b/test/interop/op-supervisor/op_supervisor_launcher_test.star new file mode 100644 index 00000000..c061aef0 --- /dev/null +++ b/test/interop/op-supervisor/op_supervisor_launcher_test.star @@ -0,0 +1,259 @@ +main = import_module("/main.star") + +interop_constants = import_module("/src/interop/constants.star") + + +def get_l2_cl_services(plan): + services = plan.get_services() + + return [s for s in services if s.name.startswith("op-cl-")] + + +def get_supervisor_services_by_service_name(plan): + services = plan.get_services() + + return { + s.name: s + for s in services + if s.name.startswith(interop_constants.SUPERVISOR_SERVICE_NAME) + } + + +def expect_no_supervisors(plan): + supervisor_services = get_supervisor_services_by_service_name(plan) + + expect.eq(supervisor_services, {}) + + +def test_op_supervisor_disabled(plan): + main.run( + plan, + { + "optimism_package": { + "chains": [{}], + "interop": { + "enabled": False, + }, + }, + }, + ) + + expect_no_supervisors(plan) + + +def test_op_supervisor_interop_set_disabled(plan): + main.run( + plan, + { + "optimism_package": { + "chains": [{}], + "interop": { + "enabled": True, + "sets": [ + { + "enabled": False, + } + ], + }, + }, + }, + ) + + expect_no_supervisors(plan) + + +def test_op_supervisor_interop_set_no_participants(plan): + main.run( + plan, + { + "optimism_package": { + "chains": [{}], + "interop": { + "enabled": True, + "sets": [ + { + "enabled": True, + "participants": [], + } + ], + }, + }, + }, + ) + + expect_no_supervisors(plan) + + +def test_op_supervisor_single_interop_set(plan): + main.run( + plan, + { + "optimism_package": { + "chains": [ + { + "network_params": { + "network_id": "1", + } + } + ], + "interop": { + "enabled": True, + "sets": [ + { + "enabled": True, + "name": "great-interop-set-what-a-name", + "participants": "*", + } + ], + }, + }, + }, + ) + + cl_services = get_l2_cl_services(plan) + cl_services_urls = [ + "ws://{0}:{1}".format( + cl_service.ip_address, + interop_constants.INTEROP_WS_PORT_NUM, + ) + for cl_service in cl_services + ] + + supervisor_services = get_supervisor_services_by_service_name(plan) + supervisor_service = supervisor_services[ + "op-supervisor-great-interop-set-what-a-name" + ] + expect.ne(supervisor_service, None) + + supervisor_service_config = kurtosistest.get_service_config(supervisor_service.name) + expect.ne(supervisor_service_config, None) + + expect.eq( + supervisor_service_config.image, + "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-supervisor:develop", + ) + expect.eq( + supervisor_service_config.env_vars["OP_SUPERVISOR_L2_CONSENSUS_NODES"], + ",".join(cl_services_urls), + ) + + +def test_op_supervisor_multiple_interop_sets(plan): + main.run( + plan, + { + "optimism_package": { + "chains": [ + { + "network_params": { + "network_id": "1000", + } + }, + { + "network_params": { + "network_id": "2000", + } + }, + { + "network_params": { + "network_id": "3000", + } + }, + { + "participants": [{}, {}], + "network_params": { + "network_id": "4000", + }, + }, + ], + "interop": { + "enabled": True, + "sets": [ + { + "enabled": True, + "name": "great-interop-set-what-a-name", + "participants": ["1000", "3000"], + }, + { + "enabled": True, + "name": "even-better-interop", + "participants": ["2000"], + "supervisor_params": { + "image": "op-supervisor:latest", + "extra_params": ["--foo", "--bar"], + }, + }, + { + "enabled": True, + "name": "but-wait-theres-more", + "participants": ["4000"], + }, + ], + }, + }, + }, + ) + + supervisor_services = get_supervisor_services_by_service_name(plan) + + supervisor_service1 = supervisor_services[ + "op-supervisor-great-interop-set-what-a-name" + ] + expect.ne(supervisor_service1, None) + + supervisor_service2 = supervisor_services["op-supervisor-even-better-interop"] + expect.ne(supervisor_service2, None) + + supervisor_service3 = supervisor_services["op-supervisor-but-wait-theres-more"] + expect.ne(supervisor_service3, None) + + cl_services = get_l2_cl_services(plan) + cl_services_urls_by_service_name = { + s.name: "ws://{0}:{1}".format( + s.ip_address, + interop_constants.INTEROP_WS_PORT_NUM, + ) + for s in cl_services + } + + supervisor_service1_config = kurtosistest.get_service_config( + supervisor_service1.name + ) + expect.eq( + supervisor_service1_config.env_vars["OP_SUPERVISOR_L2_CONSENSUS_NODES"], + ",".join( + [ + cl_services_urls_by_service_name[ + "op-cl-1000-1-op-node-op-geth-op-kurtosis" + ], + cl_services_urls_by_service_name[ + "op-cl-3000-1-op-node-op-geth-op-kurtosis" + ], + ] + ), + ) + + supervisor_service2_config = kurtosistest.get_service_config( + supervisor_service2.name + ) + expect.eq( + supervisor_service2_config.env_vars["OP_SUPERVISOR_L2_CONSENSUS_NODES"], + cl_services_urls_by_service_name["op-cl-2000-1-op-node-op-geth-op-kurtosis"], + ) + + supervisor_service3_config = kurtosistest.get_service_config( + supervisor_service3.name + ) + expect.eq( + supervisor_service3_config.env_vars["OP_SUPERVISOR_L2_CONSENSUS_NODES"], + ",".join( + [ + cl_services_urls_by_service_name[ + "op-cl-4000-1-op-node-op-geth-op-kurtosis" + ], + cl_services_urls_by_service_name[ + "op-cl-4000-2-op-node-op-geth-op-kurtosis" + ], + ] + ), + ) diff --git a/test/package_io/input_parser_test.star b/test/package_io/input_parser_test.star index b8c19356..91c6b78a 100644 --- a/test/package_io/input_parser_test.star +++ b/test/package_io/input_parser_test.star @@ -91,197 +91,359 @@ def test_external_l1_network_params_input_parser_set_fields(plan): expect.eq(parsed_params, struct(**params)) + def test_interop_default_args(plan): parsed_params = input_parser.parse_network_params(plan, {}) expect.eq(parsed_params["interop"], input_parser.default_interop_params()) + def test_interop_supervisor_params(plan): - supervisor_args = { "image": "supervisor.jpeg", "dependency_set": None, "extra_params": None } - parsed_params = input_parser.parse_network_params(plan, { - "interop": { - "supervisor_params": supervisor_args, + supervisor_args = { + "image": "supervisor.jpeg", + "dependency_set": None, + "extra_params": None, + } + parsed_params = input_parser.parse_network_params( + plan, + { + "interop": { + "supervisor_params": supervisor_args, + }, }, - }) + ) - expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { - "supervisor_params": { - "image": "supervisor.jpeg", - "dependency_set": "", - "extra_params": [], + expect.eq( + parsed_params["interop"], + input_parser.default_interop_params() + | { + "supervisor_params": { + "image": "supervisor.jpeg", + "dependency_set": "", + "extra_params": [], + }, }, - }) + ) + def test_interop_default_set_params(plan): - supervisor_args = { "image": "supervisor.jpeg" } - parsed_params = input_parser.parse_network_params(plan, { - "interop": { - "supervisor_params": supervisor_args, - "sets": [{}] + supervisor_args = {"image": "supervisor.jpeg"} + parsed_params = input_parser.parse_network_params( + plan, + { + "interop": {"supervisor_params": supervisor_args, "sets": [{}]}, + }, + ) + + expect.eq( + parsed_params["interop"], + input_parser.default_interop_params() + | { + "enabled": True, + "supervisor_params": input_parser.default_supervisor_params() + | supervisor_args, + "sets": [ + { + "enabled": True, + "participants": ["2151908"], + "name": "interop-set-0", + "supervisor_params": input_parser.default_supervisor_params() + | supervisor_args, + } + ], }, - }) - - expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { - "enabled": True, - "supervisor_params": input_parser.default_supervisor_params() | supervisor_args, - "sets": [{ - "participants": ["2151908"], - "name": "interop-set-0", - "supervisor_params": input_parser.default_supervisor_params() | supervisor_args - }] - }) + ) + def test_interop_set_params(plan): - supervisor_args = { "image": "supervisor.jpeg" } - parsed_params = input_parser.parse_network_params(plan, { - "interop": { - "supervisor_params": supervisor_args, - "sets": [{}] + supervisor_args = {"image": "supervisor.jpeg"} + parsed_params = input_parser.parse_network_params( + plan, + { + "interop": {"supervisor_params": supervisor_args, "sets": [{}]}, + }, + ) + + expect.eq( + parsed_params["interop"], + input_parser.default_interop_params() + | { + "enabled": True, + "supervisor_params": input_parser.default_supervisor_params() + | supervisor_args, + "sets": [ + { + "enabled": True, + "participants": ["2151908"], + "name": "interop-set-0", + "supervisor_params": input_parser.default_supervisor_params() + | supervisor_args, + } + ], }, - }) - - expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { - "enabled": True, - "supervisor_params": input_parser.default_supervisor_params() | supervisor_args, - "sets": [{ - "participants": ["2151908"], - "name": "interop-set-0", - "supervisor_params": input_parser.default_supervisor_params() | supervisor_args - }] - }) + ) + def test_interop_set_all_participants_by_default(plan): - parsed_params = input_parser.parse_network_params(plan, { - "chains": [{ - "network_params": { - "network_id": "network-0", - }, - }, { - "network_params": { - "network_id": "network-1", - }, - }], - "interop": { - "sets": [{}] + parsed_params = input_parser.parse_network_params( + plan, + { + "chains": [ + { + "network_params": { + "network_id": "network-0", + }, + }, + { + "network_params": { + "network_id": "network-1", + }, + }, + ], + "interop": {"sets": [{}]}, }, - }) + ) + + expect.eq( + parsed_params["interop"], + input_parser.default_interop_params() + | { + "enabled": True, + "sets": [ + { + "enabled": True, + "name": "interop-set-0", + "participants": ["network-0", "network-1"], + "supervisor_params": input_parser.default_supervisor_params(), + } + ], + }, + ) - expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { - "enabled": True, - "sets": [{ - "name": "interop-set-0", - "participants": ["network-0", "network-1"], - "supervisor_params": input_parser.default_supervisor_params() - }] - }) def test_interop_set_all_participants_explicitly(plan): - parsed_params = input_parser.parse_network_params(plan, { - "chains": [{ - "network_params": { - "network_id": "network-0", - }, - }, { - "network_params": { - "network_id": "network-1", - }, - }], - "interop": { - "sets": [{ - "participants": "*" - }] + parsed_params = input_parser.parse_network_params( + plan, + { + "chains": [ + { + "network_params": { + "network_id": "network-0", + }, + }, + { + "network_params": { + "network_id": "network-1", + }, + }, + ], + "interop": {"sets": [{"participants": "*"}]}, }, - }) + ) + + expect.eq( + parsed_params["interop"], + input_parser.default_interop_params() + | { + "enabled": True, + "sets": [ + { + "enabled": True, + "name": "interop-set-0", + "participants": ["network-0", "network-1"], + "supervisor_params": input_parser.default_supervisor_params(), + } + ], + }, + ) - expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { - "enabled": True, - "sets": [{ - "name": "interop-set-0", - "participants": ["network-0", "network-1"], - "supervisor_params": input_parser.default_supervisor_params() - }] - }) # This test tests an invalid configuration that we want to support to model misconfiguration -# +# # In this test case we have two interop sets that both contain the same network def test_interop_set_all_participants_multiple_times(plan): - parsed_params = input_parser.parse_network_params(plan, { - "chains": [{ - "network_params": { - "network_id": "network-0", - }, - }], - "interop": { - "sets": [{}, {}] + parsed_params = input_parser.parse_network_params( + plan, + { + "chains": [ + { + "network_params": { + "network_id": "network-0", + }, + } + ], + "interop": {"sets": [{}, {}]}, }, - }) - - expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { - "enabled": True, - "sets": [{ - "name": "interop-set-0", - "participants": ["network-0"], - "supervisor_params": input_parser.default_supervisor_params(), - }, { - "name": "interop-set-1", - "participants": ["network-0"], - "supervisor_params": input_parser.default_supervisor_params(), - }] - }) + ) + + expect.eq( + parsed_params["interop"], + input_parser.default_interop_params() + | { + "enabled": True, + "sets": [ + { + "enabled": True, + "name": "interop-set-0", + "participants": ["network-0"], + "supervisor_params": input_parser.default_supervisor_params(), + }, + { + "enabled": True, + "name": "interop-set-1", + "participants": ["network-0"], + "supervisor_params": input_parser.default_supervisor_params(), + }, + ], + }, + ) + + +def test_interop_set_duplicate_names(plan): + expect.fails( + lambda: input_parser.parse_network_params( + plan, + { + "chains": [ + { + "network_params": { + "network_id": "network-0", + }, + } + ], + "interop": { + "sets": [ + { + "name": "interop-set-0", + "participants": [], + }, + { + "name": "interop-set-0", + "participants": [], + }, + ] + }, + }, + ), + "fail: Duplicate interop set names: interop-set-0", + ) + def test_interop_set_duplicate_participants(plan): - expect.fails(lambda: input_parser.parse_network_params(plan, { - "chains": [{ - "network_params": { - "network_id": "network-0", + expect.fails( + lambda: input_parser.parse_network_params( + plan, + { + "chains": [ + { + "network_params": { + "network_id": "network-0", + }, + } + ], + "interop": { + "sets": [ + { + "participants": ["network-0", "network-0"], + } + ] + }, }, - }], - "interop": { - "sets": [{ - "participants": ["network-0", "network-0"], - }] - }, - }), "Duplicate network ids in list of interop participants: \\[\"network-0\"\\]") + ), + "fail: Duplicate network ids in list of interop participants: network-0", + ) + def test_interop_set_nonexistent_participants(plan): - expect.fails(lambda: input_parser.parse_network_params(plan, { - "chains": [{ - "network_params": { - "network_id": "network-0", + expect.fails( + lambda: input_parser.parse_network_params( + plan, + { + "chains": [ + { + "network_params": { + "network_id": "network-0", + }, + } + ], + "interop": { + "sets": [ + { + "participants": ["network-1"], + } + ] + }, }, - }], - "interop": { - "sets": [{ - "participants": ["network-1"], - }] - }, - }), "Unknown network id in list of interop participants: network-1") + ), + "Unknown network id in list of interop participants: network-1", + ) + def test_interop_set_none_params(plan): - parsed_params = input_parser.parse_network_params(plan, { - "interop": { - "sets": [None] + parsed_params = input_parser.parse_network_params( + plan, + { + "interop": {"sets": [None]}, }, - }) + ) + + expect.eq( + parsed_params["interop"], input_parser.default_interop_params() | {"sets": []} + ) - expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { - "sets": [] - }) def test_interop_explicitly_disabled(plan): - parsed_params = input_parser.parse_network_params(plan, { - "interop": { + parsed_params = input_parser.parse_network_params( + plan, + { + "interop": {"enabled": False, "sets": [{}]}, + }, + ) + + expect.eq( + parsed_params["interop"], + input_parser.default_interop_params() + | { "enabled": False, - "sets": [{}] + "sets": [ + { + "enabled": True, + "name": "interop-set-0", + "participants": ["2151908"], + "supervisor_params": input_parser.default_supervisor_params(), + } + ], }, - }) + ) - expect.eq(parsed_params["interop"], input_parser.default_interop_params() | { - "enabled": False, - "sets": [{ - "name": "interop-set-0", - "participants": ["2151908"], - "supervisor_params": input_parser.default_supervisor_params() - }] - }) \ No newline at end of file + +def test_interop__set_explicitly_disabled(plan): + parsed_params = input_parser.parse_network_params( + plan, + { + "interop": { + "enabled": True, + "sets": [ + { + "enabled": False, + } + ], + }, + }, + ) + + expect.eq( + parsed_params["interop"], + input_parser.default_interop_params() + | { + "enabled": True, + "sets": [ + { + "enabled": False, + "name": "interop-set-0", + "participants": ["2151908"], + "supervisor_params": input_parser.default_supervisor_params(), + } + ], + }, + ) diff --git a/test/util_test.star b/test/util_test.star index c8e7b321..1d254b9b 100644 --- a/test/util_test.star +++ b/test/util_test.star @@ -59,15 +59,17 @@ def test_label_from_image_long_image_name_long_suffix(plan): expect.eq(len(image_label), 63) expect.eq(image_suffix[-63:], image_label) + def test_filter_none(plan): expect.eq(util.filter_none({}), {}) - expect.eq(util.filter_none({ "key": "value" }), { "key": "value" }) - expect.eq(util.filter_none({ "key": None }), {}) - expect.eq(util.filter_none({ "key": False }), { "key": False }) - expect.eq(util.filter_none({ "key": 0 }), { "key": 0 }) - expect.eq(util.filter_none({ "key": "" }), { "key": "" }) - expect.eq(util.filter_none({ "key": [] }), { "key": [] }) - expect.eq(util.filter_none({ "key": {} }), { "key": {} }) + expect.eq(util.filter_none({"key": "value"}), {"key": "value"}) + expect.eq(util.filter_none({"key": None}), {}) + expect.eq(util.filter_none({"key": False}), {"key": False}) + expect.eq(util.filter_none({"key": 0}), {"key": 0}) + expect.eq(util.filter_none({"key": ""}), {"key": ""}) + expect.eq(util.filter_none({"key": []}), {"key": []}) + expect.eq(util.filter_none({"key": {}}), {"key": {}}) + def test_get_duplicates(plan): expect.eq(util.get_duplicates([]), []) From f26ddd9a66fc09505826bb87362515c81734b7c3 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 21 Apr 2025 19:55:00 -0700 Subject: [PATCH 08/14] chore: Adjust readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index d2571856..5088dee9 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,10 @@ optimism_package: image: "grafana/grafana:11.5.0" # Interop configuration interop: + # Interop can be enabled and disabled using this flag + # + # By default, interop will be enabled if there is at least one interop set specified + enabled: false # A list of interop sets - connected L2 chains # # If left empty, interop is disabled @@ -147,6 +151,10 @@ optimism_package: # # Leave empty to use the default supervisor configuration supervisor_params: + # Interop set can be disabled for ease of local development + # + # Defaults to true + enabled: true - name: "interop-set-1" # "*" can be used to quickly add all networks into one interop set participants: "*" From dd81b3e363480d776c781a17009b8ec48fc2955a Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 21 Apr 2025 20:01:07 -0700 Subject: [PATCH 09/14] fix: Forgotten debug --- src/l2.star | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/l2.star b/src/l2.star index eefc0e99..c5cd85a8 100644 --- a/src/l2.star +++ b/src/l2.star @@ -48,8 +48,6 @@ def launch_l2( ) plan.print("Successfully launched da-server") - kurtosistest.debug(l2_num) - l2 = participant_network.launch_participant_network( plan, l2_args.participants, From 396ee82cb6c49f9dd0d03ecea88439a186ebecfa Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 21 Apr 2025 20:03:04 -0700 Subject: [PATCH 10/14] chore: Rename --- src/package_io/input_parser.star | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/package_io/input_parser.star b/src/package_io/input_parser.star index caba4c0a..7f2927ee 100644 --- a/src/package_io/input_parser.star +++ b/src/package_io/input_parser.star @@ -451,7 +451,7 @@ def parse_network_params(plan, input_args): # configure interop - results["interop"] = build_interop_params(input_args.get("interop", {}), chains) + results["interop"] = compile_interop_params(input_args.get("interop", {}), chains) # configure op-deployer @@ -548,15 +548,15 @@ def default_supervisor_params(): # This function normalizes the interop args to ensure that all values are set -def build_interop_params(interop_args, chains): +def compile_interop_params(interop_args, chains): # We first filter the None values so that we can merge dicts easily interop_args_without_none = util.filter_none(interop_args) # Then we build the sub-params - supervisor_params = build_interop_supervisor_params( + supervisor_params = compile_interop_supervisor_params( interop_args_without_none.get("supervisor_params", {}) ) - sets_params = build_interop_sets_params( + sets_params = compile_interop_sets_params( interop_args_without_none.get("sets", []), supervisor_params, chains ) @@ -578,7 +578,7 @@ def build_interop_params(interop_args, chains): # # - to build the default interop supervisor params, in which case the default values are the global defaults # - to build the interop supervisor params for each interop set, in which case the default values are the interop supervisor params -def build_interop_supervisor_params( +def compile_interop_supervisor_params( interop_supervisor_args, default_interop_supervisor_params=default_supervisor_params(), ): @@ -598,9 +598,9 @@ def build_interop_supervisor_params( # This function normalizes the interop sets args to ensure that all values are set -def build_interop_sets_params(interop_sets_args, interop_supervisor_params, chains): +def compile_interop_sets_params(interop_sets_args, interop_supervisor_params, chains): interop_sets_params = [ - build_interop_set_params( + compile_interop_set_params( interop_set_args, interop_set_index, interop_supervisor_params, chains ) for interop_set_index, interop_set_args in enumerate(interop_sets_args) @@ -621,7 +621,7 @@ def build_interop_sets_params(interop_sets_args, interop_supervisor_params, chai # This function normalizes the interop sets args to ensure that all values are set -def build_interop_set_params( +def compile_interop_set_params( interop_set_args, # The suffix is used to create a default name for the interop set if no name is provided interop_set_suffix, @@ -648,7 +648,7 @@ def build_interop_set_params( ) # The interop set supervisor params are optional and default to the global interop supervisor params - interop_set_supervisor_params = build_interop_supervisor_params( + interop_set_supervisor_params = compile_interop_supervisor_params( interop_set_args_without_none.get("supervisor_params", {}), interop_supervisor_params, ) From 344867f428341124ab89278a5f3a513b8f142c36 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 21 Apr 2025 20:07:53 -0700 Subject: [PATCH 11/14] chore: Typo --- test/package_io/input_parser_test.star | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/package_io/input_parser_test.star b/test/package_io/input_parser_test.star index 91c6b78a..4adaa5ad 100644 --- a/test/package_io/input_parser_test.star +++ b/test/package_io/input_parser_test.star @@ -417,7 +417,7 @@ def test_interop_explicitly_disabled(plan): ) -def test_interop__set_explicitly_disabled(plan): +def test_interop_set_explicitly_disabled(plan): parsed_params = input_parser.parse_network_params( plan, { From e84f9dace1ab9a70a96098ca593f6386da42eb7f Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 21 Apr 2025 20:15:41 -0700 Subject: [PATCH 12/14] chore: One more test --- .../op_supervisor_launcher_test.star | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test/interop/op-supervisor/op_supervisor_launcher_test.star b/test/interop/op-supervisor/op_supervisor_launcher_test.star index c061aef0..7dbf3558 100644 --- a/test/interop/op-supervisor/op_supervisor_launcher_test.star +++ b/test/interop/op-supervisor/op_supervisor_launcher_test.star @@ -257,3 +257,84 @@ def test_op_supervisor_multiple_interop_sets(plan): ] ), ) + +def test_op_supervisor_everything_in_one_set(plan): + main.run( + plan, + { + "optimism_package": { + "chains": [ + { + "network_params": { + "network_id": "1000", + } + }, + { + "network_params": { + "network_id": "2000", + } + }, + { + "network_params": { + "network_id": "3000", + } + }, + { + "participants": [{}, {}], + "network_params": { + "network_id": "4000", + }, + }, + ], + "interop": { + "sets": [ + { + "name": "the-interopest-of-sets", + "participants": "*", + }, + ], + }, + }, + }, + ) + + supervisor_services = get_supervisor_services_by_service_name(plan) + supervisor_service = supervisor_services[ + "op-supervisor-the-interopest-of-sets" + ] + expect.ne(supervisor_service, None) + + cl_services = get_l2_cl_services(plan) + cl_services_urls_by_service_name = { + s.name: "ws://{0}:{1}".format( + s.ip_address, + interop_constants.INTEROP_WS_PORT_NUM, + ) + for s in cl_services + } + + supervisor_service_config = kurtosistest.get_service_config( + supervisor_service.name + ) + expect.eq( + supervisor_service_config.env_vars["OP_SUPERVISOR_L2_CONSENSUS_NODES"], + ",".join( + [ + cl_services_urls_by_service_name[ + "op-cl-1000-1-op-node-op-geth-op-kurtosis" + ], + cl_services_urls_by_service_name[ + "op-cl-2000-1-op-node-op-geth-op-kurtosis" + ], + cl_services_urls_by_service_name[ + "op-cl-3000-1-op-node-op-geth-op-kurtosis" + ], + cl_services_urls_by_service_name[ + "op-cl-4000-1-op-node-op-geth-op-kurtosis" + ], + cl_services_urls_by_service_name[ + "op-cl-4000-2-op-node-op-geth-op-kurtosis" + ], + ] + ), + ) From f60a3c6a1f4e8ddda338654b6d3986df98a4f9c3 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 21 Apr 2025 22:59:04 -0700 Subject: [PATCH 13/14] fix: Lint --- .../op-supervisor/op_supervisor_launcher_test.star | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/interop/op-supervisor/op_supervisor_launcher_test.star b/test/interop/op-supervisor/op_supervisor_launcher_test.star index 7dbf3558..861d301b 100644 --- a/test/interop/op-supervisor/op_supervisor_launcher_test.star +++ b/test/interop/op-supervisor/op_supervisor_launcher_test.star @@ -258,6 +258,7 @@ def test_op_supervisor_multiple_interop_sets(plan): ), ) + def test_op_supervisor_everything_in_one_set(plan): main.run( plan, @@ -299,9 +300,7 @@ def test_op_supervisor_everything_in_one_set(plan): ) supervisor_services = get_supervisor_services_by_service_name(plan) - supervisor_service = supervisor_services[ - "op-supervisor-the-interopest-of-sets" - ] + supervisor_service = supervisor_services["op-supervisor-the-interopest-of-sets"] expect.ne(supervisor_service, None) cl_services = get_l2_cl_services(plan) @@ -313,9 +312,7 @@ def test_op_supervisor_everything_in_one_set(plan): for s in cl_services } - supervisor_service_config = kurtosistest.get_service_config( - supervisor_service.name - ) + supervisor_service_config = kurtosistest.get_service_config(supervisor_service.name) expect.eq( supervisor_service_config.env_vars["OP_SUPERVISOR_L2_CONSENSUS_NODES"], ",".join( From 50dd2b3ea6c3bb9c882c1a825796aff3a40c3d1b Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Tue, 22 Apr 2025 07:23:24 -0700 Subject: [PATCH 14/14] chore: Dependency set name --- .../op-supervisor/op_supervisor_launcher.star | 12 +++++++++--- .../op-supervisor/op_supervisor_launcher_test.star | 12 ++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/interop/op-supervisor/op_supervisor_launcher.star b/src/interop/op-supervisor/op_supervisor_launcher.star index fa205153..115a9702 100644 --- a/src/interop/op-supervisor/op_supervisor_launcher.star +++ b/src/interop/op-supervisor/op_supervisor_launcher.star @@ -27,7 +27,10 @@ def get_used_ports(): DATA_DIR = "/etc/op-supervisor" -DEPENDENCY_SET_FILE_NAME = "dependency_set.json" + + +def create_dependency_set_filename(interop_set): + return "dependency_set-{}.json".format(interop_set.name) def create_dependency_set(l2s): @@ -79,8 +82,9 @@ def launch( ) # And write it to an artifact + dependency_set_filename = create_dependency_set_filename(interop_set) dependency_set_artifact = utils.write_to_file( - plan, dependency_set_json, DATA_DIR, DEPENDENCY_SET_FILE_NAME + plan, dependency_set_json, DATA_DIR, dependency_set_filename ) # We create a service name based on the interop set name @@ -94,6 +98,7 @@ def launch( l2s=interop_set_l2s, jwt_file=jwt_file, dependency_set_artifact=dependency_set_artifact, + dependency_set_filename=dependency_set_filename, supervisor_params=interop_set.supervisor_params, observability_helper=observability_helper, ) @@ -114,6 +119,7 @@ def get_supervisor_config( l2s, jwt_file, dependency_set_artifact, + dependency_set_filename, supervisor_params, observability_helper, ): @@ -136,7 +142,7 @@ def get_supervisor_config( env_vars={ "OP_SUPERVISOR_DATADIR": "/db", "OP_SUPERVISOR_DEPENDENCY_SET": "{0}/{1}".format( - DATA_DIR, DEPENDENCY_SET_FILE_NAME + DATA_DIR, dependency_set_filename ), "OP_SUPERVISOR_L1_RPC": l1_config_env_vars["L1_RPC_URL"], "OP_SUPERVISOR_L2_CONSENSUS_NODES": ",".join( diff --git a/test/interop/op-supervisor/op_supervisor_launcher_test.star b/test/interop/op-supervisor/op_supervisor_launcher_test.star index 861d301b..34c2b8f4 100644 --- a/test/interop/op-supervisor/op_supervisor_launcher_test.star +++ b/test/interop/op-supervisor/op_supervisor_launcher_test.star @@ -1,6 +1,9 @@ main = import_module("/main.star") interop_constants = import_module("/src/interop/constants.star") +op_supervisor_launcher = import_module( + "/src/interop/op-supervisor/op_supervisor_launcher.star" +) def get_l2_cl_services(plan): @@ -335,3 +338,12 @@ def test_op_supervisor_everything_in_one_set(plan): ] ), ) + + +def test_create_dependency_set_filename(plan): + expect.eq( + op_supervisor_launcher.create_dependency_set_filename( + struct(name="my-interop-set") + ), + "dependency_set-my-interop-set.json", + )