diff --git a/README.md b/README.md index 1cb22ec8..5088dee9 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,31 @@ optimism_package: image: "grafana/grafana:11.5.0" # Interop configuration interop: - # Whether or not to enable interop mode + # 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 + sets: + # Optional human-readable name for this interop set + - 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: + # 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: "*" # Default supervisor configuration supervisor_params: # The Docker image that should be used for the supervisor; leave blank to use the default op-supervisor image 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..115a9702 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") @@ -27,18 +27,21 @@ def get_used_ports(): DATA_DIR = "/etc/op-supervisor" -DEPENDENCY_SET_FILE_NAME = "dependency_set.json" -def create_dependency_set(chains): +def create_dependency_set_filename(interop_set): + return "dependency_set-{}.json".format(interop_set.name) + + +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 +49,68 @@ 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_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 + 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, + dependency_set_filename=dependency_set_filename, + 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( @@ -88,6 +119,7 @@ def get_supervisor_config( l2s, jwt_file, dependency_set_artifact, + dependency_set_filename, supervisor_params, observability_helper, ): @@ -110,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/src/package_io/input_parser.star b/src/package_io/input_parser.star index 8b3b63bd..7f2927ee 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", @@ -128,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"], @@ -312,16 +328,6 @@ def parse_network_params(plan, input_args): input_args.get("observability", {}).get("grafana_params", {}) ) - # 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", {}) - ) - # configure altda results["altda_deploy_config"] = default_altda_deploy_config() @@ -443,6 +449,10 @@ def parse_network_params(plan, input_args): results["chains"] = chains + # configure interop + + results["interop"] = compile_interop_params(input_args.get("interop", {}), chains) + # configure op-deployer results["op_contract_deployer_params"] = default_op_contract_deployer_params() @@ -520,9 +530,171 @@ 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. + "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 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 = compile_interop_supervisor_params( + interop_args_without_none.get("supervisor_params", {}) + ) + sets_params = compile_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 + "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 compile_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 compile_interop_sets_params(interop_sets_args, interop_supervisor_params, chains): + interop_sets_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) + 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 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, + 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 + ) + + # 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 = compile_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} + + # "*" 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( + ",".join(duplicate_network_ids) + ) + ) + + return network_ids + else: + fail("Invalid interop set participants: {}".format(interop_set_participants)) + + def default_altda_deploy_config(): return { "use_altda": False, @@ -534,14 +706,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/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 336cb6b5..0e282583 100644 --- a/src/util.star +++ b/src/util.star @@ -144,3 +144,24 @@ 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} + + +# 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 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..34c2b8f4 --- /dev/null +++ b/test/interop/op-supervisor/op_supervisor_launcher_test.star @@ -0,0 +1,349 @@ +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): + 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" + ], + ] + ), + ) + + +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" + ], + ] + ), + ) + + +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", + ) diff --git a/test/package_io/input_parser_test.star b/test/package_io/input_parser_test.star index 5bc22d2a..4adaa5ad 100644 --- a/test/package_io/input_parser_test.star +++ b/test/package_io/input_parser_test.star @@ -90,3 +90,360 @@ 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() + | { + "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, + } + ], + }, + ) + + +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() + | { + "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, + } + ], + }, + ) + + +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": [ + { + "enabled": True, + "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": [ + { + "enabled": True, + "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": [ + { + "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", + }, + } + ], + "interop": { + "sets": [ + { + "participants": ["network-0", "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", + }, + } + ], + "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": {"sets": [None]}, + }, + ) + + 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": [ + { + "enabled": True, + "name": "interop-set-0", + "participants": ["2151908"], + "supervisor_params": input_parser.default_supervisor_params(), + } + ], + }, + ) + + +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 9f9a7caa..1d254b9b 100644 --- a/test/util_test.star +++ b/test/util_test.star @@ -58,3 +58,25 @@ 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": {}}) + + +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]])