diff --git a/src/el/op-rbuilder/launcher.star b/src/el/op-rbuilder/launcher.star new file mode 100644 index 00000000..44cb731f --- /dev/null +++ b/src/el/op-rbuilder/launcher.star @@ -0,0 +1,247 @@ +_ethereum_package_el_context = import_module( + "github.com/ethpandaops/ethereum-package/src/el/el_context.star" +) +_ethereum_package_el_admin_node_info = import_module( + "github.com/ethpandaops/ethereum-package/src/el/el_admin_node_info.star" +) + +_ethereum_package_input_parser = import_module( + "github.com/ethpandaops/ethereum-package/src/package_io/input_parser.star" +) + +_ethereum_package_constants = import_module( + "github.com/ethpandaops/ethereum-package/src/package_io/constants.star" +) + +_filter = import_module("/src/util/filter.star") +_net = import_module("/src/util/net.star") + +_constants = import_module("../../package_io/constants.star") + +_observability = import_module("../../observability/observability.star") + +# Paths +_METRICS_PATH = "/metrics" + +# The dirpath of the execution data directory on the client container +_EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER = "/data/op-reth/execution-data" + + +_VERBOSITY_LEVELS = { + _ethereum_package_constants.GLOBAL_LOG_LEVEL.error: "v", + _ethereum_package_constants.GLOBAL_LOG_LEVEL.warn: "vv", + _ethereum_package_constants.GLOBAL_LOG_LEVEL.info: "vvv", + _ethereum_package_constants.GLOBAL_LOG_LEVEL.debug: "vvvv", + _ethereum_package_constants.GLOBAL_LOG_LEVEL.trace: "vvvvv", +} + + +def launch( + plan, + params, + network_params, + sequencer_params, + jwt_file, + deployment_output, + log_level, + persistent, + tolerations, + node_selectors, + bootnode_contexts, + observability_helper, + supervisors_params, +): + el_log_level = _ethereum_package_input_parser.get_client_log_level_or_default( + params.log_level, log_level, _VERBOSITY_LEVELS + ) + + config = get_service_config( + plan=plan, + params=params, + network_params=network_params, + sequencer_params=sequencer_params, + jwt_file=jwt_file, + deployment_output=deployment_output, + log_level=el_log_level, + persistent=persistent, + tolerations=tolerations, + node_selectors=node_selectors, + bootnode_contexts=bootnode_contexts, + observability_helper=observability_helper, + supervisors_params=supervisors_params, + ) + + service = plan.add_service(params.service_name, config) + + engine_rpc_port = params.ports[_net.ENGINE_RPC_PORT_NAME] + rpc_port = params.ports[_net.RPC_PORT_NAME] + ws_port = params.ports[_net.WS_PORT_NAME] + rpc_url = _net.service_url(params.service_name, rpc_port) + + enode = _ethereum_package_el_admin_node_info.get_enode_for_node( + plan, params.service_name, _net.RPC_PORT_NAME + ) + + metrics_info = _observability.new_metrics_info( + observability_helper, service, _METRICS_PATH + ) + + return struct( + service=service, + context=_ethereum_package_el_context.new_el_context( + client_name="op-rbuilder", + enode=enode, + ip_addr=service.ip_address, + rpc_port_num=rpc_port.number, + ws_port_num=ws_port.number, + engine_rpc_port_num=engine_rpc_port.number, + rpc_http_url=rpc_url, + service_name=params.service_name, + el_metrics_info=[metrics_info], + ), + ) + + +def get_service_config( + plan, + params, + network_params, + sequencer_params, + jwt_file, + deployment_output, + log_level, + persistent, + tolerations, + node_selectors, + bootnode_contexts, + observability_helper, + supervisors_params, +): + ports = _net.ports_to_port_specs(params.ports) + + cmd = [ + "node", + "-{0}".format(log_level), + "--datadir={}".format(_EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER), + "--chain={0}".format( + network_params.network + if network_params.network in _ethereum_package_constants.PUBLIC_NETWORKS + else _ethereum_package_constants.GENESIS_CONFIG_MOUNT_PATH_ON_CONTAINER + + "/genesis-{0}.json".format(network_params.network_id) + ), + "--http", + "--http.port={0}".format(ports[_net.RPC_PORT_NAME].number), + "--http.addr=0.0.0.0", + "--http.corsdomain=*", + # WARNING: The admin info endpoint is enabled so that we can easily get ENR/enode, which means + # that users should NOT store private information in these Kurtosis nodes! + "--http.api=admin,net,eth,web3,debug,trace", + "--ws", + "--ws.addr=0.0.0.0", + "--ws.port={0}".format(ports[_net.WS_PORT_NAME].number), + "--ws.api=net,eth", + "--ws.origins=*", + "--nat=extip:{}".format( + _ethereum_package_constants.PRIVATE_IP_ADDRESS_PLACEHOLDER + ), + "--authrpc.port={0}".format(ports[_net.ENGINE_RPC_PORT_NAME].number), + "--authrpc.jwtsecret={}".format( + _ethereum_package_constants.JWT_MOUNT_PATH_ON_CONTAINER + ), + "--authrpc.addr=0.0.0.0", + "--discovery.port={0}".format(ports[_net.TCP_DISCOVERY_PORT_NAME].number), + "--port={0}".format(ports[_net.TCP_DISCOVERY_PORT_NAME].number), + "--rpc.eth-proof-window=302400", + ] + + # configure files + + files = { + _ethereum_package_constants.GENESIS_DATA_MOUNTPOINT_ON_CLIENTS: deployment_output, + _ethereum_package_constants.JWT_MOUNTPOINT_ON_CLIENTS: jwt_file, + } + + if persistent: + files[_EXECUTION_DATA_DIRPATH_ON_CLIENT_CONTAINER] = Directory( + persistent_key="data-{0}".format(params.service_name), + size=int(params.volume_size) + if int(params.volume_size) > 0 + else _constants.VOLUME_SIZE[network_params.network][ + _constants.EL_TYPE.op_rbuilder + "_volume_size" + ], + ) + + # configure environment variables + + env_vars = dict(params.extra_env_vars) + + # apply customizations + + if observability_helper.enabled: + cmd.append("--metrics=0.0.0.0:{0}".format(_observability.METRICS_PORT_NUM)) + + _observability.expose_metrics_port(ports) + + if sequencer_params: + cmd.append( + "--rollup.sequencer-http={0}".format( + _net.service_url( + sequencer_params.el.service_name, + sequencer_params.el.ports[_net.RPC_PORT_NAME], + ) + ) + ) + + if len(bootnode_contexts) > 0: + cmd.append( + "--bootnodes=" + + ",".join( + [ + ctx.enode + for ctx in bootnode_contexts[ + : _ethereum_package_constants.MAX_ENODE_ENTRIES + ] + ] + ) + ) + + cmd += params.extra_params + + if params.key: + cmd.append("--rollup.builder-secret-key={0}".format(params.key)) + + supervisor_params = _filter.first(supervisors_params) + if supervisor_params: + cmd.append( + "--rollup.supervisor-http={0}".format( + _net.service_url( + supervisor_params.service_name, + supervisor_params.ports[_net.RPC_PORT_NAME], + ) + ) + ) + + config_args = { + "image": params.image, + "ports": ports, + "cmd": cmd, + "files": files, + "private_ip_address_placeholder": _ethereum_package_constants.PRIVATE_IP_ADDRESS_PLACEHOLDER, + "env_vars": env_vars, + "labels": params.labels, + "tolerations": tolerations, + "node_selectors": node_selectors, + } + + # configure resources + + if params.min_cpu > 0: + config_args["min_cpu"] = params.min_cpu + if params.max_cpu > 0: + config_args["max_cpu"] = params.max_cpu + if params.min_mem > 0: + config_args["min_memory"] = params.min_mem + if params.max_mem > 0: + config_args["max_memory"] = params.max_mem + + return ServiceConfig(**config_args) diff --git a/src/l2/participant/cl/input_parser.star b/src/l2/participant/cl/input_parser.star index c4352ad1..7a66d4a0 100644 --- a/src/l2/participant/cl/input_parser.star +++ b/src/l2/participant/cl/input_parser.star @@ -25,15 +25,18 @@ _IMAGE_IDS = { } -def parse(args, participant_name, network_id, registry): - return _parse(args, participant_name, network_id, registry, "cl") +def parse(args, participant_name, network_params, registry): + return _parse(args, participant_name, network_params, registry, "cl") -def parse_builder(args, participant_name, network_id, registry): - return _parse(args, participant_name, network_id, registry, "cl_builder") +def parse_builder(args, participant_name, network_params, registry): + return _parse(args, participant_name, network_params, registry, "cl_builder") -def _parse(args, participant_name, network_id, registry, cl_kind): +def _parse(args, participant_name, network_params, registry, cl_kind): + network_id = network_params.network_id + network_name = network_params.name + # Any extra attributes will cause an error _filter.assert_keys( args or {}, @@ -41,7 +44,7 @@ def _parse(args, participant_name, network_id, registry, cl_kind): "Invalid attributes in CL configuration for " + participant_name + " on network " - + str(network_id) + + network_name + ": {}", ) diff --git a/src/l2/participant/el/input_parser.star b/src/l2/participant/el/input_parser.star index 1a23aaf9..6c82b360 100644 --- a/src/l2/participant/el/input_parser.star +++ b/src/l2/participant/el/input_parser.star @@ -18,6 +18,8 @@ _DEFAULT_ARGS = { "max_mem": 0, } +_DEFAULT_BUILDER_ARGS = _DEFAULT_ARGS | {"key": None} + # EL clients have a type property that maps to an image _IMAGE_IDS = { "op-geth": _registry.OP_GETH, @@ -29,29 +31,46 @@ _IMAGE_IDS = { } -def parse(args, participant_name, network_id, registry): - return _parse(args, participant_name, network_id, registry, "el") +def parse(el_args, participant_name, network_params, registry): + return _parse( + el_args=el_args, + default_args=_DEFAULT_ARGS, + participant_name=participant_name, + network_params=network_params, + registry=registry, + el_kind="el", + ) + +def parse_builder(el_args, participant_name, network_params, registry): + return _parse( + el_args=el_args, + default_args=_DEFAULT_BUILDER_ARGS, + participant_name=participant_name, + network_params=network_params, + registry=registry, + el_kind="el_builder", + ) -def parse_builder(args, participant_name, network_id, registry): - return _parse(args, participant_name, network_id, registry, "el_builder") +def _parse(el_args, default_args, participant_name, network_params, registry, el_kind): + network_id = network_params.network_id + network_name = network_params.name -def _parse(args, participant_name, network_id, registry, el_kind): # Any extra attributes will cause an error _filter.assert_keys( - args or {}, - _DEFAULT_ARGS.keys(), + el_args or {}, + default_args.keys(), "Invalid attributes in EL configuration for " + participant_name + " on network " - + str(network_id) + + network_name + ": {}", ) # We filter the None values so that we can merge dicts easily # and merge the config with the defaults - el_params = _DEFAULT_ARGS | _filter.remove_none(args or {}) + el_params = default_args | _filter.remove_none(el_args or {}) # We default the image to the one in the registry # diff --git a/src/l2/participant/el/launcher.star b/src/l2/participant/el/launcher.star index 9c898585..e7d9c966 100644 --- a/src/l2/participant/el/launcher.star +++ b/src/l2/participant/el/launcher.star @@ -3,6 +3,7 @@ _op_besu_launcher = import_module("/src/el/op-besu/launcher.star") _op_erigon_launcher = import_module("/src/el/op-erigon/launcher.star") _op_geth_launcher = import_module("/src/el/op-geth/launcher.star") _op_nethermind_launcher = import_module("/src/el/op-nethermind/launcher.star") +_op_rbuilder_launcher = import_module("/src/el/op-rbuilder/launcher.star") _op_reth_launcher = import_module("/src/el/op-reth/launcher.star") _filter = import_module("/src/util/filter.star") @@ -89,8 +90,24 @@ def launch( observability_helper=observability_helper, supervisors_params=supervisors_params, ) + elif params.type == "op-rbuilder": + el = _op_rbuilder_launcher.launch( + plan=plan, + params=params, + network_params=network_params, + sequencer_params=sequencer_params, + jwt_file=jwt_file, + deployment_output=deployment_output, + log_level=log_level, + persistent=persistent, + tolerations=tolerations, + node_selectors=node_selectors, + bootnode_contexts=bootnode_contexts, + observability_helper=observability_helper, + supervisors_params=supervisors_params, + ) elif params.type == "op-reth": - el = _op_geth_launcher.launch( + el = _op_reth_launcher.launch( plan=plan, params=params, network_params=network_params, diff --git a/src/l2/participant/input_parser.star b/src/l2/participant/input_parser.star index 62a786cd..2cd3fccb 100644 --- a/src/l2/participant/input_parser.star +++ b/src/l2/participant/input_parser.star @@ -117,16 +117,16 @@ def _parse_instance(participant_args, participant_name, network_params, registry return struct( el=_el_input_parser.parse( - participant_params["el"], participant_name, network_id, registry + participant_params["el"], participant_name, network_params, registry ), el_builder=_el_input_parser.parse_builder( - participant_params["el_builder"], participant_name, network_id, registry + participant_params["el_builder"], participant_name, network_params, registry ), cl=_cl_input_parser.parse( - participant_params["cl"], participant_name, network_id, registry + participant_params["cl"], participant_name, network_params, registry ), cl_builder=_cl_input_parser.parse_builder( - participant_params["cl_builder"], participant_name, network_id, registry + participant_params["cl_builder"], participant_name, network_params, registry ), name=participant_name, sequencer=sequencer, diff --git a/src/package_io/constants.star b/src/package_io/constants.star index 97a9461f..12984d9b 100644 --- a/src/package_io/constants.star +++ b/src/package_io/constants.star @@ -13,6 +13,7 @@ EL_TYPE = struct( op_erigon="op-erigon", op_nethermind="op-nethermind", op_besu="op-besu", + op_rbuilder="op-rbuilder", op_reth="op-reth", ) @@ -32,6 +33,7 @@ VOLUME_SIZE = { "op-erigon_volume_size": 3000, # 3GB "op-nethermind_volume_size": 3000, # 3GB "op-besu_volume_size": 3000, # 3GB + "op-rbuilder_volume_size": 3000, # 3GB "op-reth_volume_size": 3000, # 3GB "op-node_volume_size": 1000, # 1GB "hildr_volume_size": 1000, # 1GB diff --git a/test/l2/participant/el/launcher_test.star b/test/l2/participant/el/launcher_test.star index 81dd99fd..137740da 100644 --- a/test/l2/participant/el/launcher_test.star +++ b/test/l2/participant/el/launcher_test.star @@ -373,6 +373,103 @@ def test_l2_participant_el_launcher_op_nethermind(plan): ) +def test_l2_participant_el_launcher_op_rbuilder(plan): + # We'll need the observability params from the legacy parser + legacy_params = _input_parser.input_parser( + plan=plan, + input_args={}, + ) + observability_helper = _observability.make_helper(legacy_params.observability) + + l2s_params = _l2_input_parser.parse( + { + "network0": { + "participants": { + "node0": { + "el_builder": {"type": "op-rbuilder", "key": "secret key"} + } + } + } + }, + registry=_default_registry, + ) + + l2_params = l2s_params[0] + participant_params = l2_params.participants[0] + el_params = participant_params.el_builder + + sequencer_private_key_mock = "sequencer_private_key" + kurtosistest.mock(_util, "read_network_config_value").mock_return_value( + sequencer_private_key_mock + ) + + result = _el_launcher.launch( + plan=plan, + params=el_params, + network_params=l2_params.network_params, + supervisors_params=[], + sequencer_params=None, + jwt_file=_default_jwt_file, + deployment_output=_default_deployment_output, + bootnode_contexts=_default_bootnode_contexts, + log_level=_default_log_level, + persistent=True, + tolerations=[], + node_selectors={}, + observability_helper=observability_helper, + ) + + service = plan.get_service(el_params.service_name) + service_config = kurtosistest.get_service_config(el_params.service_name) + + expect.eq( + service_config.cmd, + [ + "node", + "-vvv", + "--datadir=/data/op-reth/execution-data", + "--chain=/network-configs/genesis-2151908.json", + "--http", + "--http.port=8545", + "--http.addr=0.0.0.0", + "--http.corsdomain=*", + "--http.api=admin,net,eth,web3,debug,trace", + "--ws", + "--ws.addr=0.0.0.0", + "--ws.port=8546", + "--ws.api=net,eth", + "--ws.origins=*", + "--nat=extip:KURTOSIS_IP_ADDR_PLACEHOLDER", + "--authrpc.port=8551", + "--authrpc.jwtsecret=/jwt/jwtsecret", + "--authrpc.addr=0.0.0.0", + "--discovery.port=30303", + "--port=30303", + "--rpc.eth-proof-window=302400", + "--metrics=0.0.0.0:9001", + "--bootnodes=enode:001", + "--rollup.builder-secret-key=secret key", + ], + ) + expect.eq( + service_config.labels, + { + "op.kind": "el_builder", + "op.network.id": "2151908", + "op.network.participant.name": "node0", + "op.el.type": "op-rbuilder", + }, + ) + expect.eq( + service_config.files["/network-configs"].artifact_names, + [_default_deployment_output], + ) + expect.eq( + service_config.files["/jwt"].artifact_names, + [_default_jwt_file], + ) + + def test_l2_participant_el_launcher_op_reth(plan): # We'll need the observability params from the legacy parser legacy_params = _input_parser.input_parser( @@ -427,7 +524,29 @@ def test_l2_participant_el_launcher_op_reth(plan): expect.eq( service_config.cmd, [ - "geth init --datadir=/data/geth/execution-data --state.scheme=hash /network-configs/genesis-2151908.json && geth --networkid=2151908 --datadir=/data/geth/execution-data --gcmode=archive --state.scheme=hash --http --http.addr=0.0.0.0 --http.vhosts=* --http.corsdomain=* --http.api=admin,engine,net,eth,web3,debug,miner --ws --ws.addr=0.0.0.0 --ws.port=8546 --ws.api=admin,engine,net,eth,web3,debug,miner --ws.origins=* --allow-insecure-unlock --authrpc.port=8551 --authrpc.addr=0.0.0.0 --authrpc.vhosts=* --authrpc.jwtsecret=/jwt/jwtsecret --syncmode=full --nat=extip:KURTOSIS_IP_ADDR_PLACEHOLDER --rpc.allow-unprotected-txs --discovery.port=30303 --port=30303 --metrics --metrics.addr=0.0.0.0 --metrics.port=9001 --bootnodes=enode:001" + "node", + "-vvv", + "--datadir=/data/op-reth/execution-data", + "--chain=/network-configs/genesis-2151908.json", + "--http", + "--http.port=8545", + "--http.addr=0.0.0.0", + "--http.corsdomain=*", + "--http.api=admin,net,eth,web3,debug,trace", + "--ws", + "--ws.addr=0.0.0.0", + "--ws.port=8546", + "--ws.api=net,eth", + "--ws.origins=*", + "--nat=extip:KURTOSIS_IP_ADDR_PLACEHOLDER", + "--authrpc.port=8551", + "--authrpc.jwtsecret=/jwt/jwtsecret", + "--authrpc.addr=0.0.0.0", + "--discovery.port=30303", + "--port=30303", + "--rpc.eth-proof-window=302400", + "--metrics=0.0.0.0:9001", + "--bootnodes=enode:001", ], ) expect.eq( diff --git a/test/l2/participant/input_parser_test.star b/test/l2/participant/input_parser_test.star index 6dc2a8fb..b555ad6c 100644 --- a/test/l2/participant/input_parser_test.star +++ b/test/l2/participant/input_parser_test.star @@ -146,6 +146,7 @@ def test_l2_participant_input_parser_defaults(plan): ), _net.ENGINE_RPC_PORT_NAME: _net.port(number=8551), }, + key=None, **_shared_defaults, ), mev_params=struct( @@ -251,6 +252,7 @@ def test_l2_participant_input_parser_defaults(plan): ), _net.ENGINE_RPC_PORT_NAME: _net.port(number=8551), }, + key=None, **_shared_defaults, ), mev_params=struct( @@ -275,6 +277,25 @@ def test_l2_participant_input_parser_defaults(plan): ) +def test_l2_participant_input_parser_el_builder_key(plan): + parsed = input_parser.parse( + {"node0": {"el_builder": {"key": "secret key"}}}, + _default_network_params, + _default_registry, + ) + + expect.eq(parsed[0].el_builder.key, "secret key") + + expect.fails( + lambda: input_parser.parse( + {"node0": {"el": {"key": "secret key"}}}, + _default_network_params, + _default_registry, + ), + "Invalid attributes in EL configuration for node0 on network my-l2: key", + ) + + def test_l2_participant_input_parser_defaults_conductor_enabled(plan): parsed = input_parser.parse( {"node0": {"conductor_params": {"enabled": True}}},