From db6461eaf064a0d81a9168163558911912954d17 Mon Sep 17 00:00:00 2001 From: NeoPlays <80448387+NeoPlays@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:01:20 +0200 Subject: [PATCH 1/5] FIX: LidoObolExitService overwriting shared charon & ejector volume permissions --- .../update-changes/molecule/2410/converge.yml | 10 ++ .../update-changes/molecule/2410/molecule.yml | 28 +++ .../update-changes/molecule/2410/playbook.yml | 7 + .../update-changes/molecule/2410/prepare.yml | 160 ++++++++++++++++++ .../update-changes/molecule/2410/verify.yml | 66 ++++++++ .../tasks/2.4.10/obol_exit_changes.yaml | 39 +++++ .../tasks/2.4.10/updates-2410.yaml | 11 ++ controls/roles/update-changes/tasks/main.yml | 4 + .../ethereum-services/LidoObolExitService.js | 2 +- 9 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 controls/roles/update-changes/molecule/2410/converge.yml create mode 100644 controls/roles/update-changes/molecule/2410/molecule.yml create mode 100644 controls/roles/update-changes/molecule/2410/playbook.yml create mode 100644 controls/roles/update-changes/molecule/2410/prepare.yml create mode 100644 controls/roles/update-changes/molecule/2410/verify.yml create mode 100644 controls/roles/update-changes/tasks/2.4.10/obol_exit_changes.yaml create mode 100644 controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml diff --git a/controls/roles/update-changes/molecule/2410/converge.yml b/controls/roles/update-changes/molecule/2410/converge.yml new file mode 100644 index 0000000000..3f84cb4a24 --- /dev/null +++ b/controls/roles/update-changes/molecule/2410/converge.yml @@ -0,0 +1,10 @@ +--- +- name: Converge + hosts: all + vars_files: + - ../../../../defaults/stereum_defaults.yaml + + tasks: + - name: "Include update-changes" + include_role: + name: "update-changes" diff --git a/controls/roles/update-changes/molecule/2410/molecule.yml b/controls/roles/update-changes/molecule/2410/molecule.yml new file mode 100644 index 0000000000..775a62d81e --- /dev/null +++ b/controls/roles/update-changes/molecule/2410/molecule.yml @@ -0,0 +1,28 @@ +--- +#dependency: +# name: galaxy +driver: + name: docker +platforms: + - name: "update-changes--2.4.10--ubuntu-26.04" + image: ubuntu:resolute + # - name: "configure-updates--default--centos-8" + # image: geerlingguy/docker-centos8-ansible +provisioner: + name: ansible + env: + ANSIBLE_PIPELINING: "True" + ANSIBLE_ROLES_PATH: "${MOLECULE_PROJECT_DIRECTORY}/..:./roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles" +lint: | + set -e + yamllint . + ansible-lint . +scenario: + test_sequence: + - destroy + - create + - prepare + - converge + - idempotence + - verify + - destroy diff --git a/controls/roles/update-changes/molecule/2410/playbook.yml b/controls/roles/update-changes/molecule/2410/playbook.yml new file mode 100644 index 0000000000..710c596cf6 --- /dev/null +++ b/controls/roles/update-changes/molecule/2410/playbook.yml @@ -0,0 +1,7 @@ +--- +- name: Converge + hosts: all + tasks: + - name: "Include update-changes" + include_role: + name: "update-changes" diff --git a/controls/roles/update-changes/molecule/2410/prepare.yml b/controls/roles/update-changes/molecule/2410/prepare.yml new file mode 100644 index 0000000000..46ed2dd41f --- /dev/null +++ b/controls/roles/update-changes/molecule/2410/prepare.yml @@ -0,0 +1,160 @@ +--- +- name: Prepare + hosts: all + tasks: + - name: Make sure Stereum's config path exists + file: + path: "/etc/stereum/services" + state: directory + owner: "root" + group: "root" + mode: 0644 + become: yes + + - name: Create LidoObolExitService config (running as root - the bug) + copy: + dest: "/etc/stereum/services/39b887e5-a4a0-2322-4bc9-43d42d9f5bd6.yaml" + owner: "root" + group: "root" + mode: 0644 + content: | + service: LidoObolExitService + id: 39b887e5-a4a0-2322-4bc9-43d42d9f5bd6 + configVersion: 1 + command: + - run + - --beacon-node-url=http://stereum-6c2c02b0-7918-a378-0c6a-91c41214f93f:5051 + - --charon-runtime-dir=/charon + - --ejector-exit-path=/exitmessages + - --exit-epoch=256 + - --log-color=auto + - --log-format=console + - --log-level=info + - --obol-api-url=https://api.obol.tech + - --validator-query-chunk-size=50 + entrypoint: + - /usr/local/bin/lido-dv-exit + env: {} + image: obolnetwork/lido-dv-exit:v0.5.2 + ports: [] + volumes: + - /opt/stereum/validatorejector-8dbf517f-1b92-ad18-b9c4-1b4f43ef8dbe/messages:/exitmessages + - /opt/stereum/charon-d1f91f40-2b3b-20a6-d381-65ac39db9af3/.charon:/charon + user: root + autoupdate: true + network: hoodi + dependencies: + executionClients: [] + consensusClients: + - service: TekuBeaconService + id: 6c2c02b0-7918-a378-0c6a-91c41214f93f + mevboost: [] + otherServices: + - service: ValidatorEjectorService + id: 8dbf517f-1b92-ad18-b9c4-1b4f43ef8dbe + - service: CharonService + id: d1f91f40-2b3b-20a6-d381-65ac39db9af3 + become: yes + + - name: Create ValidatorEjectorService config + copy: + dest: "/etc/stereum/services/8dbf517f-1b92-ad18-b9c4-1b4f43ef8dbe.yaml" + owner: "root" + group: "root" + mode: 0644 + content: | + service: ValidatorEjectorService + id: 8dbf517f-1b92-ad18-b9c4-1b4f43ef8dbe + configVersion: 1 + command: [] + entrypoint: [] + env: + EXECUTION_NODE: http://stereum-57277de9-a1e8-b4bb-6b8f-f4417905d9b4:8545 + CONSENSUS_NODE: http://stereum-6c2c02b0-7918-a378-0c6a-91c41214f93f:5051 + LOCATOR_ADDRESS: "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8" + STAKING_MODULE_ID: "1" + OPERATOR_ID: "123456789" + MESSAGES_LOCATION: /app/messages + ORACLE_ADDRESSES_ALLOWLIST: '["0xcA80ee7313A315879f326105134F938676Cfd7a9","0xf03B8DC8762B97F13Ac82e6F94bE3Ed002FF7459","0x1932f53B1457a5987791a40Ba91f71c5Efd5788F","0xf7aE520e99ed3C41180B5E12681d31Aa7302E4e5","0x99B2B75F490fFC9A29E4E1f5987BE8e30E690aDF","0x219743f1911d84B32599BdC2Df21fC8Dba6F81a2","0xD3b1e36A372Ca250eefF61f90E833Ca070559970","0x4c75FA734a39f3a21C57e583c1c29942F021C6B7","0x948A62cc0414979dc7aa9364BA5b96ECb29f8736","0xfe43A8B0b481Ae9fB1862d31826532047d2d538c","0x43C45C2455C49eed320F463fF4f1Ece3D2BF5aE2"]' + HTTP_PORT: "8989" + RUN_METRICS: "true" + RUN_HEALTH_CHECK: "true" + DRY_RUN: "false" + image: lidofinance/validator-ejector:2.1.0 + ports: [] + volumes: + - /opt/stereum/validatorejector-8dbf517f-1b92-ad18-b9c4-1b4f43ef8dbe/messages:/app/messages + user: "2000" + autoupdate: true + network: hoodi + dependencies: + executionClients: + - service: NethermindService + id: 57277de9-a1e8-b4bb-6b8f-f4417905d9b4 + consensusClients: + - service: TekuBeaconService + id: 6c2c02b0-7918-a378-0c6a-91c41214f93f + mevboost: [] + otherServices: [] + become: yes + + - name: Create CharonService config + copy: + dest: "/etc/stereum/services/d1f91f40-2b3b-20a6-d381-65ac39db9af3.yaml" + owner: "root" + group: "root" + mode: 0644 + content: | + service: CharonService + id: d1f91f40-2b3b-20a6-d381-65ac39db9af3 + configVersion: 1 + command: + - run + - --beacon-node-endpoints=http://stereum-6c2c02b0-7918-a378-0c6a-91c41214f93f:5051 + - --log-level=info + - --log-format=console + - --p2p-relays=https://0.relay.obol.tech + - --p2p-tcp-address=0.0.0.0:3610 + - --validator-api-address=0.0.0.0:3600 + - --monitoring-address=0.0.0.0:3620 + - --builder-api + entrypoint: + - /usr/local/bin/charon + env: {} + image: obolnetwork/charon:v1.10.3 + ports: + - 0.0.0.0:3610:3610/tcp + volumes: + - /opt/stereum/charon-d1f91f40-2b3b-20a6-d381-65ac39db9af3:/opt/charon + user: "2000" + autoupdate: true + network: hoodi + dependencies: + executionClients: [] + consensusClients: + - service: TekuBeaconService + id: 6c2c02b0-7918-a378-0c6a-91c41214f93f + mevboost: [] + otherServices: [] + become: yes + + # Simulate the damage: the root LidoObolExitService has recursively + # chowned the shared charon + ejector volumes to root:root. + - name: Simulate broken ownership on shared ejector volume + file: + path: "/opt/stereum/validatorejector-8dbf517f-1b92-ad18-b9c4-1b4f43ef8dbe/messages" + state: directory + owner: "root" + group: "root" + recurse: yes + become: yes + + - name: Simulate broken ownership on shared charon volume + file: + path: "/opt/stereum/charon-d1f91f40-2b3b-20a6-d381-65ac39db9af3/.charon" + state: directory + owner: "root" + group: "root" + recurse: yes + become: yes +# EOF diff --git a/controls/roles/update-changes/molecule/2410/verify.yml b/controls/roles/update-changes/molecule/2410/verify.yml new file mode 100644 index 0000000000..f0dfa0f411 --- /dev/null +++ b/controls/roles/update-changes/molecule/2410/verify.yml @@ -0,0 +1,66 @@ +--- +- name: Verify + hosts: all + gather_facts: false + tasks: + # LidoObolExitService: user must have been changed from root to 2000 + - name: Read LidoObolExitService file + slurp: + src: "/etc/stereum/services/39b887e5-a4a0-2322-4bc9-43d42d9f5bd6.yaml" + register: obol_service_configuration_raw + + - name: Parse LidoObolExitService configuration + set_fact: + obol_service_configuration: "{{ obol_service_configuration_raw['content'] | b64decode | from_yaml }}" + + - debug: + msg: "{{ obol_service_configuration }}" + + - name: Assert LidoObolExitService now runs as user 2000 + assert: + that: + - obol_service_configuration.user == "2000" + fail_msg: "LidoObolExitService user was not changed to 2000 (got: {{ obol_service_configuration.user }})" + + # Shared volume ownership must be repaired back to uid 2000 + - name: Stat shared ejector messages volume + stat: + path: "/opt/stereum/validatorejector-8dbf517f-1b92-ad18-b9c4-1b4f43ef8dbe/messages" + register: ejector_messages_dir + become: yes + + - name: Stat shared charon .charon volume + stat: + path: "/opt/stereum/charon-d1f91f40-2b3b-20a6-d381-65ac39db9af3/.charon" + register: charon_data_dir + become: yes + + - name: Assert shared volumes are owned by uid 2000 again + assert: + that: + - ejector_messages_dir.stat.uid == 2000 + - charon_data_dir.stat.uid == 2000 + fail_msg: "Shared volume ownership was not repaired to uid 2000 (ejector: {{ ejector_messages_dir.stat.uid }}, charon: {{ charon_data_dir.stat.uid }})" + + # The other two services must be left untouched (still 2000) + - name: Read ValidatorEjectorService file + slurp: + src: "/etc/stereum/services/8dbf517f-1b92-ad18-b9c4-1b4f43ef8dbe.yaml" + register: ejector_service_configuration_raw + + - name: Read CharonService file + slurp: + src: "/etc/stereum/services/d1f91f40-2b3b-20a6-d381-65ac39db9af3.yaml" + register: charon_service_configuration_raw + + - name: Parse ValidatorEjectorService and CharonService configurations + set_fact: + ejector_service_configuration: "{{ ejector_service_configuration_raw['content'] | b64decode | from_yaml }}" + charon_service_configuration: "{{ charon_service_configuration_raw['content'] | b64decode | from_yaml }}" + + - name: Assert ValidatorEjectorService and CharonService still run as 2000 + assert: + that: + - ejector_service_configuration.user == "2000" + - charon_service_configuration.user == "2000" +# EOF diff --git a/controls/roles/update-changes/tasks/2.4.10/obol_exit_changes.yaml b/controls/roles/update-changes/tasks/2.4.10/obol_exit_changes.yaml new file mode 100644 index 0000000000..fdc17c7679 --- /dev/null +++ b/controls/roles/update-changes/tasks/2.4.10/obol_exit_changes.yaml @@ -0,0 +1,39 @@ +--- +- name: Read service file + slurp: + src: "{{ config_file.path }}" + register: service_configuration_raw + +- name: Parse service's configuration + set_fact: + service_configuration: "{{ service_configuration_raw['content'] | b64decode | from_yaml }}" + +- name: Fix LidoObolExitService user + when: + - service_configuration.service == "LidoObolExitService" + - service_configuration.user == "root" + block: + # LidoObolExitService shares its volumes (charon's .charon dir and the + # validator ejector's messages dir) with services that run as user 2000. + # Running it as root made manage-service recursively chown those shared + # volumes to root on every start/stop, breaking charon and the ejector. + - name: Set user to 2000 in configuration + lineinfile: + path: "{{ config_file.path }}" + regexp: '^user:' + line: 'user: "2000"' + become: yes + + - name: Repair ownership of shared volumes broken by the root user + file: + path: "{{ item.split(':') | first }}" + state: directory + owner: "2000" + group: "2000" + recurse: yes + become: yes + when: + - item.split(':') | first != '/' + - item.split(':') | first != '/sys' + - item.split(':') | first != '/proc' + with_items: "{{ service_configuration.volumes }}" diff --git a/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml b/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml new file mode 100644 index 0000000000..382575d84e --- /dev/null +++ b/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml @@ -0,0 +1,11 @@ +--- +- name: Find service configs + find: + paths: "/etc/stereum/services" + register: service_config_files + +- name: Include Obol Exit Changes + include_tasks: obol_exit_changes.yaml + loop: "{{ service_config_files.files }}" + loop_control: + loop_var: config_file diff --git a/controls/roles/update-changes/tasks/main.yml b/controls/roles/update-changes/tasks/main.yml index 974a17ca88..4c9855d3ba 100644 --- a/controls/roles/update-changes/tasks/main.yml +++ b/controls/roles/update-changes/tasks/main.yml @@ -9,4 +9,8 @@ - name: Include 2.4.8 Update Scripts include_tasks: "2.4.8/updates-248.yaml" ignore_errors: true + +- name: Include 2.4.10 Update Scripts + include_tasks: "2.4.10/updates-2410.yaml" + ignore_errors: true # EOF diff --git a/launcher/src/backend/ethereum-services/LidoObolExitService.js b/launcher/src/backend/ethereum-services/LidoObolExitService.js index 94e4019c4d..53019f7437 100644 --- a/launcher/src/backend/ethereum-services/LidoObolExitService.js +++ b/launcher/src/backend/ethereum-services/LidoObolExitService.js @@ -46,7 +46,7 @@ export class LidoObolExitService extends NodeService { null, // env ports, // ports volumes, // volumes - "root", // user + "2000", // user network, // network [], // executionClients consensusClients[0] ? [consensusClients[0]] : [], // consensusClients From 654bc7ef7add96684fbd451d623d2aced5a8449c Mon Sep 17 00:00:00 2001 From: NeoPlays <80448387+NeoPlays@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:05:12 +0200 Subject: [PATCH 2/5] FIX: Teku P2P port conflict with QUIC (9001 -> 9000) --- .../update-changes/molecule/2410/prepare.yml | 57 +++++++++++++++++++ .../update-changes/molecule/2410/verify.yml | 25 ++++++++ .../tasks/2.4.10/teku_changes.yaml | 55 ++++++++++++++++++ .../tasks/2.4.10/updates-2410.yaml | 6 ++ launcher/src/backend/ServiceManager.js | 5 +- .../ethereum-services/TekuBeaconService.js | 8 +-- 6 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 controls/roles/update-changes/tasks/2.4.10/teku_changes.yaml diff --git a/controls/roles/update-changes/molecule/2410/prepare.yml b/controls/roles/update-changes/molecule/2410/prepare.yml index 46ed2dd41f..9ba609f54a 100644 --- a/controls/roles/update-changes/molecule/2410/prepare.yml +++ b/controls/roles/update-changes/molecule/2410/prepare.yml @@ -138,6 +138,63 @@ otherServices: [] become: yes + - name: Create TekuBeaconService config (P2P on 9001 - conflicts with QUIC) + copy: + dest: "/etc/stereum/services/6c2c02b0-7918-a378-0c6a-91c41214f93f.yaml" + owner: "root" + group: "root" + mode: 0644 + content: | + service: TekuBeaconService + id: 6c2c02b0-7918-a378-0c6a-91c41214f93f + configVersion: 2 + command: + - --network=hoodi + - --p2p-enabled=true + - --p2p-port=9001 + - --p2p-advertised-port=9001 + - --ee-endpoint=http://stereum-57277de9-a1e8-b4bb-6b8f-f4417905d9b4:8551 + - --ee-jwt-secret-file=/engine.jwt + - --metrics-enabled=true + - --metrics-port=8008 + - --metrics-interface=0.0.0.0 + - --metrics-host-allowlist=* + - --metrics-publish-interval=10 + - --data-path=/opt/app/data + - --data-storage-mode=minimal + - --rest-api-port=5051 + - --rest-api-host-allowlist=* + - --rest-api-interface=0.0.0.0 + - --rest-api-docs-enabled=true + - --rest-api-enabled=true + - --log-destination=CONSOLE + - --beacon-liveness-tracking-enabled=true + - --ignore-weak-subjectivity-period-enabled + entrypoint: + - /opt/teku/bin/teku + env: + JAVA_OPTS: -Xmx6g + image: consensys/teku:24.10.3 + ports: + - 0.0.0.0:9001:9001/tcp + - 0.0.0.0:9001:9001/udp + - 127.0.0.1:5052:5052/tcp + - 127.0.0.1:5051:5051/tcp + volumes: + - /opt/stereum/teku-6c2c02b0-7918-a378-0c6a-91c41214f93f/data:/opt/app/data + - /opt/stereum/nethermind-57277de9-a1e8-b4bb-6b8f-f4417905d9b4/engine.jwt:/engine.jwt + user: "2000" + autoupdate: true + network: hoodi + dependencies: + executionClients: + - service: NethermindService + id: 57277de9-a1e8-b4bb-6b8f-f4417905d9b4 + consensusClients: [] + mevboost: [] + otherServices: [] + become: yes + # Simulate the damage: the root LidoObolExitService has recursively # chowned the shared charon + ejector volumes to root:root. - name: Simulate broken ownership on shared ejector volume diff --git a/controls/roles/update-changes/molecule/2410/verify.yml b/controls/roles/update-changes/molecule/2410/verify.yml index f0dfa0f411..dcbcc314ad 100644 --- a/controls/roles/update-changes/molecule/2410/verify.yml +++ b/controls/roles/update-changes/molecule/2410/verify.yml @@ -63,4 +63,29 @@ that: - ejector_service_configuration.user == "2000" - charon_service_configuration.user == "2000" + + # TekuBeaconService: P2P must move to 9000, QUIC exposed on 9001/udp + - name: Read TekuBeaconService file + slurp: + src: "/etc/stereum/services/6c2c02b0-7918-a378-0c6a-91c41214f93f.yaml" + register: teku_service_configuration_raw + + - name: Parse TekuBeaconService configuration + set_fact: + teku_service_configuration: "{{ teku_service_configuration_raw['content'] | b64decode | from_yaml }}" + + - debug: + msg: "{{ teku_service_configuration }}" + + - name: Assert Teku P2P moved to 9000 and QUIC exposed on 9001/udp + assert: + that: + - '"--p2p-port=9000" in teku_service_configuration.command' + - '"--p2p-advertised-port=9000" in teku_service_configuration.command' + - '"--p2p-port=9001" not in teku_service_configuration.command' + - '"0.0.0.0:9000:9000/tcp" in teku_service_configuration.ports' + - '"0.0.0.0:9000:9000/udp" in teku_service_configuration.ports' + - '"0.0.0.0:9001:9001/udp" in teku_service_configuration.ports' + - '"0.0.0.0:9001:9001/tcp" not in teku_service_configuration.ports' + fail_msg: "Teku P2P/QUIC ports were not migrated correctly: {{ teku_service_configuration.ports }} / {{ teku_service_configuration.command }}" # EOF diff --git a/controls/roles/update-changes/tasks/2.4.10/teku_changes.yaml b/controls/roles/update-changes/tasks/2.4.10/teku_changes.yaml new file mode 100644 index 0000000000..89739db479 --- /dev/null +++ b/controls/roles/update-changes/tasks/2.4.10/teku_changes.yaml @@ -0,0 +1,55 @@ +--- +- name: Read service file + slurp: + src: "{{ config_file.path }}" + register: service_configuration_raw + +- name: Parse service's configuration + set_fact: + service_configuration: "{{ service_configuration_raw['content'] | b64decode | from_yaml }}" + +# New Teku versions run a QUIC transport that defaults to UDP 9001. Stereum used +# to put the P2P port on 9001 as well, so the P2P UDP port and QUIC collided. +# Move P2P back to Teku's default 9000 (tcp + udp) and expose QUIC on 9001/udp. +# Guarded on the old "--p2p-port=9001" flag so the block is a no-op once applied. +- name: Teku Beacon Port Changes + when: + - service_configuration.service == "TekuBeaconService" + - service_configuration.command is defined + - '"--p2p-port=9001" in service_configuration.command' + block: + - name: Move Teku --p2p-port to 9000 + ansible.builtin.replace: + path: "{{ config_file.path }}" + regexp: '--p2p-port=9001' + replace: '--p2p-port=9000' + + - name: Move Teku --p2p-advertised-port to 9000 + ansible.builtin.replace: + path: "{{ config_file.path }}" + regexp: '--p2p-advertised-port=9001' + replace: '--p2p-advertised-port=9000' + + - name: Move Teku P2P TCP port mapping to 9000 + ansible.builtin.replace: + path: "{{ config_file.path }}" + regexp: ':9001:9001/tcp' + replace: ':9000:9000/tcp' + + - name: Move Teku P2P UDP port mapping to 9000 + ansible.builtin.replace: + path: "{{ config_file.path }}" + regexp: ':9001:9001/udp' + replace: ':9000:9000/udp' + + - name: Add Teku QUIC UDP port mapping on 9001 + ansible.builtin.lineinfile: + path: "{{ config_file.path }}" + line: " - 0.0.0.0:9001:9001/udp" + insertafter: '^\s*-\s*.*:9000:9000/udp\s*$' + + - name: Touch service config file to trigger service restart + file: + path: "{{ config_file.path }}" + state: touch + changed_when: false # to pass idempotence test diff --git a/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml b/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml index 382575d84e..e4032a3741 100644 --- a/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml +++ b/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml @@ -9,3 +9,9 @@ loop: "{{ service_config_files.files }}" loop_control: loop_var: config_file + +- name: Include Teku Changes + include_tasks: teku_changes.yaml + loop: "{{ service_config_files.files }}" + loop_control: + loop_var: config_file diff --git a/launcher/src/backend/ServiceManager.js b/launcher/src/backend/ServiceManager.js index 484a973fd7..834b041e38 100755 --- a/launcher/src/backend/ServiceManager.js +++ b/launcher/src/backend/ServiceManager.js @@ -1129,8 +1129,9 @@ export class ServiceManager { case "TekuBeaconService": ports = [ - new ServicePort(null, 9001, 9001, servicePortProtocol.tcp), - new ServicePort(null, 9001, 9001, servicePortProtocol.udp), + new ServicePort(null, 9000, 9000, servicePortProtocol.tcp), // TCP transport for libp2p + new ServicePort(null, 9000, 9000, servicePortProtocol.udp), // discv5 peer discovery + new ServicePort(null, 9001, 9001, servicePortProtocol.udp), // QUIC transport for libp2p new ServicePort("127.0.0.1", 5052, 5052, servicePortProtocol.tcp), new ServicePort("127.0.0.1", args.port ? args.port : 5051, 5051, servicePortProtocol.tcp), ]; diff --git a/launcher/src/backend/ethereum-services/TekuBeaconService.js b/launcher/src/backend/ethereum-services/TekuBeaconService.js index 0d6877adc0..a01bb22ec7 100755 --- a/launcher/src/backend/ethereum-services/TekuBeaconService.js +++ b/launcher/src/backend/ethereum-services/TekuBeaconService.js @@ -42,8 +42,8 @@ export class TekuBeaconService extends NodeService { [ `--network=${network}`, "--p2p-enabled=true", - "--p2p-port=9001", - "--p2p-advertised-port=9001", + "--p2p-port=9000", + "--p2p-advertised-port=9000", //`--eth1-endpoints=${executionLayer}`, `--ee-endpoint=${executionLayer}`, `--ee-jwt-secret-file=${JWTDir}`, @@ -111,8 +111,8 @@ export class TekuBeaconService extends NodeService { getAvailablePorts() { return [ - new ServicePortDefinition(9001, "tcp", "P2P connections"), - new ServicePortDefinition(9001, "udp", "P2P connections"), + new ServicePortDefinition(9000, "tcp", "P2P connections"), + new ServicePortDefinition(9000, "udp", "P2P connections"), new ServicePortDefinition(5052, "tcp", "Validator API Port"), new ServicePortDefinition(5051, "tcp", "REST API Port"), new ServicePortDefinition(8008, "tcp", "METRICS Port"), From 7e9f810f3cd42d8f413cfd5792bcdf6ad6942e47 Mon Sep 17 00:00:00 2001 From: NeoPlays <80448387+NeoPlays@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:01:08 +0200 Subject: [PATCH 3/5] FIX: migrate ValidatorEjector to EJECTOR_SCOPE (STAKING_MODULE_ID/OPERATOR_ID removal) --- .../update-changes/molecule/2410/prepare.yml | 37 ++++++++++++ .../update-changes/molecule/2410/verify.yml | 32 ++++++++++ .../tasks/2.4.10/ejector_changes.yaml | 59 +++++++++++++++++++ .../tasks/2.4.10/updates-2410.yaml | 6 ++ launcher/src/backend/OneClickInstall.js | 2 +- .../ValidatorEjectorService.js | 3 +- launcher/src/store/services.js | 15 +---- 7 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 controls/roles/update-changes/tasks/2.4.10/ejector_changes.yaml diff --git a/controls/roles/update-changes/molecule/2410/prepare.yml b/controls/roles/update-changes/molecule/2410/prepare.yml index 9ba609f54a..4ba0858260 100644 --- a/controls/roles/update-changes/molecule/2410/prepare.yml +++ b/controls/roles/update-changes/molecule/2410/prepare.yml @@ -98,6 +98,43 @@ otherServices: [] become: yes + - name: Create legacy ValidatorEjectorService config (image under major v2) + copy: + dest: "/etc/stereum/services/1a2b3c4d-0000-0000-0000-000000000001.yaml" + owner: "root" + group: "root" + mode: 0644 + content: | + service: ValidatorEjectorService + id: 1a2b3c4d-0000-0000-0000-000000000001 + configVersion: 1 + command: [] + entrypoint: [] + env: + EXECUTION_NODE: http://stereum-57277de9-a1e8-b4bb-6b8f-f4417905d9b4:8545 + CONSENSUS_NODE: http://stereum-6c2c02b0-7918-a378-0c6a-91c41214f93f:5051 + LOCATOR_ADDRESS: "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8" + STAKING_MODULE_ID: "2" + OPERATOR_ID: "42" + MESSAGES_LOCATION: /app/messages + HTTP_PORT: "8989" + RUN_METRICS: "true" + RUN_HEALTH_CHECK: "true" + DRY_RUN: "false" + image: lidofinance/validator-ejector:1.5.0 + ports: [] + volumes: + - /opt/stereum/validatorejector-1a2b3c4d-0000-0000-0000-000000000001/messages:/app/messages + user: "2000" + autoupdate: true + network: holesky + dependencies: + executionClients: [] + consensusClients: [] + mevboost: [] + otherServices: [] + become: yes + - name: Create CharonService config copy: dest: "/etc/stereum/services/d1f91f40-2b3b-20a6-d381-65ac39db9af3.yaml" diff --git a/controls/roles/update-changes/molecule/2410/verify.yml b/controls/roles/update-changes/molecule/2410/verify.yml index dcbcc314ad..e0e0c246be 100644 --- a/controls/roles/update-changes/molecule/2410/verify.yml +++ b/controls/roles/update-changes/molecule/2410/verify.yml @@ -64,6 +64,38 @@ - ejector_service_configuration.user == "2000" - charon_service_configuration.user == "2000" + # ValidatorEjector (v2 image): env migrated to EJECTOR_SCOPE, image left as-is + - name: Assert v2 ejector migrated to EJECTOR_SCOPE without version bump + assert: + that: + - ejector_service_configuration.env.EJECTOR_SCOPE == '{"1":[123456789]}' + - ejector_service_configuration.env.STAKING_MODULE_ID is not defined + - ejector_service_configuration.env.OPERATOR_ID is not defined + - ejector_service_configuration.image == "lidofinance/validator-ejector:2.1.0" + fail_msg: "v2 ejector was not migrated correctly: {{ ejector_service_configuration.env }} / {{ ejector_service_configuration.image }}" + + # ValidatorEjector (legacy sub-v2 image): env migrated AND image bumped to 2.1.0 + - name: Read legacy ValidatorEjectorService file + slurp: + src: "/etc/stereum/services/1a2b3c4d-0000-0000-0000-000000000001.yaml" + register: legacy_ejector_configuration_raw + + - name: Parse legacy ValidatorEjectorService configuration + set_fact: + legacy_ejector_configuration: "{{ legacy_ejector_configuration_raw['content'] | b64decode | from_yaml }}" + + - debug: + msg: "{{ legacy_ejector_configuration }}" + + - name: Assert legacy ejector migrated to EJECTOR_SCOPE and bumped to 2.1.0 + assert: + that: + - legacy_ejector_configuration.env.EJECTOR_SCOPE == '{"2":[42]}' + - legacy_ejector_configuration.env.STAKING_MODULE_ID is not defined + - legacy_ejector_configuration.env.OPERATOR_ID is not defined + - legacy_ejector_configuration.image == "lidofinance/validator-ejector:2.1.0" + fail_msg: "legacy ejector was not migrated correctly: {{ legacy_ejector_configuration.env }} / {{ legacy_ejector_configuration.image }}" + # TekuBeaconService: P2P must move to 9000, QUIC exposed on 9001/udp - name: Read TekuBeaconService file slurp: diff --git a/controls/roles/update-changes/tasks/2.4.10/ejector_changes.yaml b/controls/roles/update-changes/tasks/2.4.10/ejector_changes.yaml new file mode 100644 index 0000000000..78240889ea --- /dev/null +++ b/controls/roles/update-changes/tasks/2.4.10/ejector_changes.yaml @@ -0,0 +1,59 @@ +--- +- name: Read service file + slurp: + src: "{{ config_file.path }}" + register: service_configuration_raw + +- name: Parse service's configuration + set_fact: + service_configuration: "{{ service_configuration_raw['content'] | b64decode | from_yaml }}" + +# validator-ejector v2 replaced the single STAKING_MODULE_ID + OPERATOR_ID pair +# with a combined EJECTOR_SCOPE map ({"":[]}). +# Convert the legacy env vars and, when the image is still under major version 2, +# bump it to a release that understands EJECTOR_SCOPE. +# Guarded on the legacy vars still being present so the block is a no-op once applied. +- name: Migrate ValidatorEjector to EJECTOR_SCOPE + when: + - service_configuration.service == "ValidatorEjectorService" + - service_configuration.env is defined + - service_configuration.env.STAKING_MODULE_ID is defined + - service_configuration.env.OPERATOR_ID is defined + block: + - name: Build EJECTOR_SCOPE and read current image tag + set_fact: + ejector_scope: '{"{{ service_configuration.env.STAKING_MODULE_ID }}":[{{ service_configuration.env.OPERATOR_ID }}]}' + ejector_image_tag: "{{ service_configuration.image.split(':')[-1] }}" + + - name: Bump validator-ejector image to 2.1.0 when under major version 2 + ansible.builtin.replace: + path: "{{ config_file.path }}" + regexp: 'lidofinance/validator-ejector:\S+' + replace: 'lidofinance/validator-ejector:2.1.0' + when: + - ejector_image_tag | regex_search('^v?[0-9]+') is not none + - (ejector_image_tag | regex_replace('^v', '') | regex_replace('\..*$', '') | int) < 2 + + - name: Add EJECTOR_SCOPE env entry + ansible.builtin.lineinfile: + path: "{{ config_file.path }}" + line: " EJECTOR_SCOPE: '{{ ejector_scope }}'" + insertafter: '^\s*LOCATOR_ADDRESS:' + + - name: Remove legacy STAKING_MODULE_ID env entry + ansible.builtin.lineinfile: + path: "{{ config_file.path }}" + regexp: '^\s*STAKING_MODULE_ID:' + state: absent + + - name: Remove legacy OPERATOR_ID env entry + ansible.builtin.lineinfile: + path: "{{ config_file.path }}" + regexp: '^\s*OPERATOR_ID:' + state: absent + + - name: Touch service config file to trigger service restart + file: + path: "{{ config_file.path }}" + state: touch + changed_when: false # to pass idempotence test diff --git a/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml b/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml index e4032a3741..acac8d630a 100644 --- a/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml +++ b/controls/roles/update-changes/tasks/2.4.10/updates-2410.yaml @@ -15,3 +15,9 @@ loop: "{{ service_config_files.files }}" loop_control: loop_var: config_file + +- name: Include Ejector EJECTOR_SCOPE Changes + include_tasks: ejector_changes.yaml + loop: "{{ service_config_files.files }}" + loop_control: + loop_var: config_file diff --git a/launcher/src/backend/OneClickInstall.js b/launcher/src/backend/OneClickInstall.js index 443a52f11a..2d6fff59a6 100755 --- a/launcher/src/backend/OneClickInstall.js +++ b/launcher/src/backend/OneClickInstall.js @@ -587,7 +587,7 @@ export class OneClickInstall { lidoobol: "2", }; let ejector = this.extraServices.find((s) => s.service === "ValidatorEjectorService"); - ejector.env.STAKING_MODULE_ID = moduleIDs[selectedPreset]; + ejector.env.EJECTOR_SCOPE = `{"${moduleIDs[selectedPreset]}":[123456789]}`; } } diff --git a/launcher/src/backend/ethereum-services/ValidatorEjectorService.js b/launcher/src/backend/ethereum-services/ValidatorEjectorService.js index 31dbeb5491..361dbd502f 100755 --- a/launcher/src/backend/ethereum-services/ValidatorEjectorService.js +++ b/launcher/src/backend/ethereum-services/ValidatorEjectorService.js @@ -48,8 +48,7 @@ export class ValidatorEjectorService extends NodeService { EXECUTION_NODE: executionClients[0] ? executionClients[0].buildExecutionClientHttpEndpointUrl() : "", CONSENSUS_NODE: consensusClients[0] ? consensusClients[0].buildConsensusClientHttpEndpointUrl() : "", LOCATOR_ADDRESS: locatorAddress, - STAKING_MODULE_ID: "1", - OPERATOR_ID: "123456789", + EJECTOR_SCOPE: '{"1":[123456789]}', MESSAGES_LOCATION: "/app/messages", ORACLE_ADDRESSES_ALLOWLIST: oracleAllowList, HTTP_PORT: "8989", diff --git a/launcher/src/store/services.js b/launcher/src/store/services.js index d012a6fddf..3234825938 100755 --- a/launcher/src/store/services.js +++ b/launcher/src/store/services.js @@ -1680,21 +1680,12 @@ export const useServices = defineStore("services", { expertOptionsModal: false, expertOptions: [ { - title: "Staking Module ID", + title: "Ejector Scope", type: "text", changeValue: null, icon: "/img/icon/service-setting-icons/Operator_ID.png", - pattern: ['(STAKING_MODULE_ID: \\")(.*)(\\"\\n)'], - commands: ["STAKING_MODULE_ID: "], - isENV: true, - }, - { - title: "Operator ID", - type: "text", - changeValue: null, - icon: "/img/icon/service-setting-icons/Operator_ID.png", - pattern: ['(OPERATOR_ID: \\")(.*)(\\"\\n)'], - commands: ["OPERATOR_ID: "], + pattern: ["(EJECTOR_SCOPE: ')(.*)('\\n)"], + commands: ["EJECTOR_SCOPE: "], isENV: true, }, ], From 47f68b74c28e404a5fef32224bd8702088f377d3 Mon Sep 17 00:00:00 2001 From: NeoPlays <80448387+NeoPlays@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:20:31 +0200 Subject: [PATCH 4/5] FIX: refresh integration-test assertions and CustomService auto-update handling --- controls/roles/manage-service/tasks/main.yml | 9 +++ .../molecule/default/prepare.yml | 20 ++++++ .../molecule/default/verify.yml | 65 +++++++++++-------- .../update-services/tasks/update-service.yml | 16 +++-- .../update-stereum/molecule/full/prepare.yml | 6 +- launcher/jest.config.js | 7 +- .../PrysmValidatorService.js | 1 + .../tests/integration/BesuService.int.js | 14 ++-- .../integration/GethOptimismService.int.js | 5 +- .../LighthouseBeaconService.int.js | 4 +- .../integration/PrysmBeaconService.int.js | 16 ++--- .../integration/TekuBeaconService.int.js | 8 ++- 12 files changed, 107 insertions(+), 64 deletions(-) diff --git a/controls/roles/manage-service/tasks/main.yml b/controls/roles/manage-service/tasks/main.yml index 1632ba8173..d169e21027 100644 --- a/controls/roles/manage-service/tasks/main.yml +++ b/controls/roles/manage-service/tasks/main.yml @@ -153,6 +153,15 @@ (stereum_service_configuration.service is match("OpRethService") and stereum_service_configuration.network is match("op-mainnet")) and (stereum.manage_service.state == "started" or stereum.manage_service.state == "restarted") + - name: Refresh image for services pinned to the "latest" tag + community.docker.docker_image_pull: + name: "{{ stereum_service_configuration.image }}" + become: yes + failed_when: false + when: + - stereum.manage_service.state == "started" or stereum.manage_service.state == "restarted" + - (stereum_service_configuration.image | split('/') | last | split(':') | last) == "latest" + - name: Start service community.docker.docker_container: command_handling: correct diff --git a/controls/roles/update-services/molecule/default/prepare.yml b/controls/roles/update-services/molecule/default/prepare.yml index 1a4d4767df..2418e1851a 100644 --- a/controls/roles/update-services/molecule/default/prepare.yml +++ b/controls/roles/update-services/molecule/default/prepare.yml @@ -97,6 +97,26 @@ network: prater become: yes + - name: Write service configuration custom (must be ignored by auto-update) + copy: + dest: "/etc/stereum/services/c0574000-0000-4000-8000-000000000000.yaml" + owner: "root" + group: "root" + mode: 0644 + content: | + service: CustomService + command: [] + entrypoint: [] + env: {} + id: c0574000-0000-4000-8000-000000000000 + image: nginx:1.27.0 + ports: [] + volumes: [] + user: 2000 + autoupdate: true + network: mainnet + become: yes + - block: - set_fact: stereum: "{{ stereum_static | combine(stereum_args, recursive=True) }}" diff --git a/controls/roles/update-services/molecule/default/verify.yml b/controls/roles/update-services/molecule/default/verify.yml index 71cfee2dce..68b026777b 100644 --- a/controls/roles/update-services/molecule/default/verify.yml +++ b/controls/roles/update-services/molecule/default/verify.yml @@ -5,32 +5,45 @@ hosts: all gather_facts: false tasks: + # Stereum config files + - name: Read service's configuration + slurp: + src: "/etc/stereum/services/9024aec6-12a8-456a-8096-ee7ef6f67167.yaml" + register: service_configuration_raw + become: yes - # Stereum config files - - name: Read service's configuration - slurp: - src: "/etc/stereum/services/9024aec6-12a8-456a-8096-ee7ef6f67167.yaml" - register: service_configuration_raw - become: yes - - - name: Parse service's configuration - set_fact: - service_configuration: "{{ service_configuration_raw['content'] | b64decode | from_yaml }}" - - - name: Check service's configuration - assert: - that: - - service_configuration.image == 'sigp/lighthouse:v2.0.1' - - - name: Get information of running updated service - community.docker.docker_container_info: - name: "stereum-9024aec6-12a8-456a-8096-ee7ef6f67167" - register: service_container - - - name: Check version of running and updated service - assert: - that: - - service_container.exists - - service_container.container.Config.Image == 'sigp/lighthouse:v2.0.0' + - name: Parse service's configuration + set_fact: + service_configuration: "{{ service_configuration_raw['content'] | b64decode | from_yaml }}" + - name: Check service's configuration + assert: + that: + - service_configuration.image == 'sigp/lighthouse:v2.0.1' + + - name: Get information of running updated service + community.docker.docker_container_info: + name: "stereum-9024aec6-12a8-456a-8096-ee7ef6f67167" + register: service_container + + - name: Check version of running and updated service + assert: + that: + - service_container.exists + - service_container.container.Config.Image == 'sigp/lighthouse:v2.0.0' + + - name: Read custom service's configuration + slurp: + src: "/etc/stereum/services/c0574000-0000-4000-8000-000000000000.yaml" + register: custom_service_configuration_raw + become: yes + + - name: Parse custom service's configuration + set_fact: + custom_service_configuration: "{{ custom_service_configuration_raw['content'] | b64decode | from_yaml }}" + + - name: Ensure CustomService image was left untouched by auto-update + assert: + that: + - custom_service_configuration.image == 'nginx:1.27.0' # EOF diff --git a/controls/roles/update-services/tasks/update-service.yml b/controls/roles/update-services/tasks/update-service.yml index 7233d3ff48..a571157fea 100644 --- a/controls/roles/update-services/tasks/update-service.yml +++ b/controls/roles/update-services/tasks/update-service.yml @@ -9,29 +9,31 @@ set_fact: service_configuration: "{{ service_configuration_raw['content'] | b64decode | from_yaml }}" +- name: Determine whether this service participates in image auto-updates + set_fact: + service_is_autoupdatable: "{{ (service_configuration.autoupdate | default(false)) and service_configuration.service != 'CustomService' }}" + - name: Extract docker image and version set_fact: service_docker_image: "{{ service_configuration.image.split(':') | first }}" service_docker_image_tag: "{{ service_configuration.image.split(':') | last }}" - when: service_configuration.autoupdate + when: service_is_autoupdatable - name: Get latest version for this service set_fact: new_service_docker_image_tag: "{{ (update_data.json.get(service_configuration.network, {}).get(service_configuration.service, update_data.json.get('mainnet', {}).get(service_configuration.service, []))) | last | default(None, true) }}" # Returns empty string if nothing is found. (don't ask me why default is not picking up and returning None) - when: - - service_configuration.autoupdate - - service_configuration.service != 'CustomService' + when: service_is_autoupdatable - name: Build new image tag set_fact: new_service_configuration_fraction: image: "{{ service_docker_image }}:{{ new_service_docker_image_tag | default(service_docker_image_tag, true) }}" # If new_service_docker_image_tag is falsy, use the current tag (will cause a restart if the tag is an empty string) - when: service_configuration.autoupdate + when: service_is_autoupdatable - name: Update configuration set_fact: new_service_configuration: "{{ service_configuration | combine(new_service_configuration_fraction) }}" - when: service_configuration.autoupdate and new_service_docker_image_tag != service_docker_image_tag + when: service_is_autoupdatable and new_service_docker_image_tag != service_docker_image_tag - name: Write configuration copy: @@ -40,6 +42,6 @@ owner: "root" group: "root" mode: 0644 - when: service_configuration.autoupdate and new_service_docker_image_tag != service_docker_image_tag + when: service_is_autoupdatable and new_service_docker_image_tag != service_docker_image_tag become: yes # EOF diff --git a/controls/roles/update-stereum/molecule/full/prepare.yml b/controls/roles/update-stereum/molecule/full/prepare.yml index 2e808fe1be..af9af6cb39 100644 --- a/controls/roles/update-stereum/molecule/full/prepare.yml +++ b/controls/roles/update-stereum/molecule/full/prepare.yml @@ -21,15 +21,11 @@ changed_when: false when: ansible_facts['distribution'] == "Ubuntu" - - name: Add source repository into sources list - apt_repository: - repo: "ppa:ansible/ansible" - - name: Install ansible (Ubuntu) apt: update_cache: yes name: - - pip + - python3-pip - ansible state: present become: true diff --git a/launcher/jest.config.js b/launcher/jest.config.js index e8be48db28..b5f8df3436 100755 --- a/launcher/jest.config.js +++ b/launcher/jest.config.js @@ -16,7 +16,10 @@ module.exports = { testEnvironmentOptions: { customExportConditions: ["node", "node-addons"], }, - collectCoverage: true, - collectCoverageFrom: ["src/**/*.{js,vue}", "!src/main.js"], + // Coverage is opt-in via the --coverage flag (test:unit / test:coverage pass it), + // so integration/other runs don't pay the cost. .vue files are excluded because + // there is no vue transformer here — instrumenting them makes babel try to parse + //