Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
24ac52f
Add CODEOWNERS file for repository ownership
F4ever Dec 9, 2025
e4be115
Merge pull request #158 from lidofinance/F4ever-patch-1
F4ever Dec 9, 2025
cd6d284
feat(fallback): include URL index in fallback log lines
stakepeter May 6, 2026
4ef76c0
feat(fallback): preserve every per-endpoint cause via AggregateError
stakepeter May 6, 2026
4017009
docs(sample.env): document shared JWT secret across EL endpoints
stakepeter May 6, 2026
14305ba
feat(el): treat JSON-RPC server errors as fallbackable
stakepeter May 6, 2026
0da3ca8
feat(consistency): chain-id consistency check at startup
stakepeter May 6, 2026
a0d43a1
chore(consistency): add explicit vitest imports to spec file
stakepeter May 6, 2026
cc6836e
docs(todo): mark survey follow-ups #34-#38 as done
stakepeter May 6, 2026
a6638f9
feat(rpc): introduce url_list validator and string[] node config
stakepeter May 6, 2026
e23d7d2
refactor(cl): centralise CL requests via clRequest, broadcast volunta…
stakepeter May 6, 2026
d64de93
feat(exit-logs): plumb VOTING_EVENTS_FRAME_BLOCKS through from config
stakepeter May 6, 2026
c28300d
chore: adapt scripts and prepare-deps to string[] node config
stakepeter May 6, 2026
a8dd27c
feat(scripts): manual fallback e2e harness
stakepeter May 6, 2026
1ea415a
chore: remove TODO.md
stakepeter May 6, 2026
e11ceb5
update env sample to use required var
May 8, 2026
0d07afc
feat: implement batch fetching of validator info and update tests
eddort May 12, 2026
853ea0b
feat: add support for multiple staking modules in configuration and s…
eddort May 12, 2026
7b5f682
fix: update Node.js version in workflow files
eddort May 13, 2026
94cbb65
feat: enhance testing setup with new e2e configurations
eddort May 13, 2026
a266cb3
Merge branch 'feature/fallback-provider' of https://github.com/stakep…
eddort May 13, 2026
0bafb03
Merge pull request #160 from stakepeter/feature/fallback-provider
eddort May 13, 2026
f985420
fix: update nock configuration to use the first URL from the consensu…
eddort May 13, 2026
9649fd4
feat: enhance logger configuration to normalize and redact multi-url …
eddort May 13, 2026
fb75ef2
test: cover fallback provider edge paths
eddort May 13, 2026
d93b9de
fix: update documentation for environment variables to clarify loggin…
eddort May 13, 2026
22b710c
feat: enhance error handling and logging for HTTP requests, including…
eddort May 14, 2026
d364c68
feat: add VALIDATORS_BATCH_SIZE configuration and update related logi…
eddort May 14, 2026
c48ac53
feat: introduce JsonRpcServerError for improved error handling and up…
eddort May 14, 2026
3bed896
feat: implement EJECTOR_SCOPE for multi-module support and refactor r…
eddort May 14, 2026
eee66dd
feat: update configuration for EJECTOR_SCOPE and deprecate legacy opt…
eddort May 14, 2026
48288f5
Merge pull request #165 from lidofinance/feat/ejector-scope-config
eddort May 14, 2026
78a1476
Merge pull request #164 from lidofinance/feat/next
eddort May 14, 2026
4f3b514
feat: add EJECTOR_SCOPE to сompose and stakingModuleId to eject logs
eddort May 15, 2026
3a36f62
feat: enhance error handling in JSON parsing
eddort May 15, 2026
b7a6401
feat: improve validation for boolean config options in makeConfig
eddort May 15, 2026
045889f
Merge pull request #161 from erl-100/envUpdate
eddort May 15, 2026
f6be96b
Merge pull request #166 from lidofinance/feat/ejector-improvements
eddort May 18, 2026
87cf864
feat: update version to 2.0.0 in package.json
eddort May 18, 2026
042b0f8
Merge pull request #163 from lidofinance/develop-next
F4ever May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* @lidofinance/lido-valset-oracles
.github @lidofinance/review-gh-workflows
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Set up node
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
node-version: '16'
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests_and_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- name: Set up node
uses: actions/setup-node@v3.5.0
with:
node-version: 'lts/*'
node-version: '16'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,14 @@ Options are configured via environment variables.

| Variable | Required | Default/Example | Description |
|-----------------------------------------------|----------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| EXECUTION_NODE | Yes | http://1.2.3.4:8545 | Ethereum Execution Node endpoint |
| CONSENSUS_NODE | Yes | http://1.2.3.4:5051 | Ethereum Consensus Node endpoint |
| JWT_SECRET_PATH | No | /data/neth/jwt.hex | Path to JWT secret hex file for Nethermind RPC authentication. If provided, will generate a new JWT token for each execution node request |
| EXECUTION_NODE | Yes | http://1.2.3.4:8545 | Ethereum Execution Node endpoint. Accepts a single URL or a comma-separated list (e.g. `http://primary:8545,http://backup:8545`). Each request tries the endpoints in order; on a 5xx, 408, 429, network or timeout error the daemon moves to the next URL. Other 4xx and validation errors are terminal (not retried across endpoints). Voluntary-exit submissions are an exception: they broadcast to every CL URL in parallel. **Cross-endpoint consistency.** At startup the daemon issues `eth_chainId` to every EL URL and `/eth/v1/config/deposit_contract` to every CL URL, and refuses to boot if the chain ids disagree. Misconfigured mixed-network URLs (e.g. mainnet + testnet pasted together) are caught at boot, not in production. |
| CONSENSUS_NODE | Yes | http://1.2.3.4:5051 | Ethereum Consensus Node endpoint. Same multi-URL semantics as `EXECUTION_NODE`. |
| JWT_SECRET_PATH | No | /data/neth/jwt.hex | Path to JWT secret hex file for Nethermind RPC authentication. If provided, will generate a new JWT token for each execution node request. The same secret is sent to every URL configured in `EXECUTION_NODE`. |
| LOCATOR_ADDRESS | Yes | 0x123 | Address of the Locator contract [Hoodi](https://docs.lido.fi/deployed-contracts/hoodi/) / [Mainnet](https://docs.lido.fi/deployed-contracts/) |
| STAKING_MODULE_ID | Yes | 123 | Staking Module ID for which operator ID is set, currently only one exists - ([NodeOperatorsRegistry](https://github.com/lidofinance/lido-dao#contracts)) with id `1` |
| OPERATOR_ID | Yes | 123 | Operator ID in the Node Operators registry, easiest to get from Operators UI: [Hoodi](https://operators-hoodi.testnet.fi/)/[Mainnet](https://operators.lido.fi) |
| OPERATOR_IDENTIFIERS | No | [0,1,2] | Alternative to OPERATOR_ID. Array of Operator IDs to handle exits for multiple operators simultaneously |
| EJECTOR_SCOPE | Yes* | {"1":[123]} | Preferred module-to-operators mapping: object keys are `stakingModuleId`, array values are `operatorIds`. Example: `{"1":[123]}` means staking module `1`, operator `123`; `{"1":[123],"4":[0,1]}` adds staking module `4` operators `0` and `1`. Replaces deprecated `STAKING_MODULE_ID`, `OPERATOR_ID`, and `OPERATOR_IDENTIFIERS`. |
| STAKING_MODULE_ID | No | 123 | Deprecated legacy single-module config. Use `EJECTOR_SCOPE` instead. |
| OPERATOR_ID | No | 123 | Deprecated legacy single-operator config. Use `EJECTOR_SCOPE` instead. |
| OPERATOR_IDENTIFIERS | No | [0,1,2] | Deprecated legacy multi-operator config for one `STAKING_MODULE_ID`. Use `EJECTOR_SCOPE` instead. |
| MESSAGES_LOCATION | No | messages | Local folder or external storage bucket url to load json exit message files from. Required if you are using exit messages mode |
| VALIDATOR_EXIT_WEBHOOK | No | http://webhook | POST validator info to an endpoint instead of sending out an exit message in order to initiate an exit. Required if you are using webhook mode |
| ORACLE_ADDRESSES_ALLOWLIST | Yes | ["0x123"] | Allowed Oracle addresses to accept transactions. List can be obtained from HashConsensus contract on [Hoodi](https://hoodi.etherscan.io/address/0x32EC59a78abaca3f91527aeB2008925D5AaC1eFC)/[Mainnet](https://etherscan.io/address/0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288) |
Expand All @@ -67,13 +68,15 @@ Options are configured via environment variables.
| MESSAGES_PASSWORD | No | password | Password to decrypt encrypted exit messages with. Needed only if you encrypt your exit messages |
| MESSAGES_PASSWORD_FILE | No | password_inside.txt | Path to a file with password inside to decrypt exit messages with. Needed only if you have encrypted exit messages. If used, MESSAGES_PASSWORD (not MESSAGES_PASSWORD_FILE) needs to be added to LOGGER_SECRETS in order to be sanitized |
| BLOCKS_PRELOAD | No | 50000 | Amount of blocks to load events from on start. Increase if daemon was not running for some time. Defaults to a week of blocks |
| VALIDATORS_BATCH_SIZE | No | 1000 | Validator IDs per Consensus API validators request batch. |
| VOTING_EVENTS_FRAME_BLOCKS | No | 216000 | How many blocks of Easy-Track motion events to look back through when security-verifying voluntary-exit requests. Default ~30 days. Lower values reduce RPC load but may skip older matching motion or voting events. |
| JOB_INTERVAL | No | 384000 | Time interval in milliseconds to run checks. Defaults to time of 1 epoch |
| HTTP_PORT | No | 8989 | Port to serve metrics and health check on |
| RUN_METRICS | No | false | Enable metrics endpoint |
| RUN_HEALTH_CHECK | No | true | Enable health check endpoint |
| LOGGER_LEVEL | No | info | Severity level from which to start showing errors eg info will hide debug messages |
| LOGGER_FORMAT | No | simple | Simple or JSON log output: simple/json |
| LOGGER_SECRETS | No | ["MESSAGES_PASSWORD"] | JSON string array of either env var keys to sanitize in logs or exact values |
| LOGGER_SECRETS | No | ["MESSAGES_PASSWORD"] | JSON array of env var names or literal values to redact from logs. `EXECUTION_NODE` and `CONSENSUS_NODE` also redact each URL from a comma-separated RPC list. |
| DRY_RUN | No | false | Run the service without actually sending out exit messages |
| TRUST_MODE | No | false | Skip security checks for exit requests. Please, use it only if you running your own RPC nodes. |
| DISABLE_SECURITY_DONT_USE_IN_PRODUCTION | No | false | Deprecated: Use TRUST_MODE instead. Skip security checks for exit requests |
Expand Down
1 change: 1 addition & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
- EXECUTION_NODE=${EXECUTION_NODE}
- CONSENSUS_NODE=${CONSENSUS_NODE}
- LOCATOR_ADDRESS=${LOCATOR_ADDRESS}
- EJECTOR_SCOPE
- STAKING_MODULE_ID=${STAKING_MODULE_ID}
- OPERATOR_ID=${OPERATOR_ID}
- MESSAGES_LOCATION=${MESSAGES_LOCATION}
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
- EXECUTION_NODE=${EXECUTION_NODE}
- CONSENSUS_NODE=${CONSENSUS_NODE}
- LOCATOR_ADDRESS=${LOCATOR_ADDRESS}
- EJECTOR_SCOPE
- STAKING_MODULE_ID=${STAKING_MODULE_ID}
- OPERATOR_ID=${OPERATOR_ID}
- MESSAGES_LOCATION=${MESSAGES_LOCATION}
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "validator-ejector",
"version": "1.9.0",
"version": "2.0.0",
"private": true,
"main": "index.js",
"license": "MIT",
Expand All @@ -26,8 +26,10 @@
"type": "module",
"scripts": {
"dev": "node --loader ts-node/esm --disable-warning=ExperimentalWarning src/index.ts",
"test": "vitest",
"coverage": "vitest run --coverage",
"test": "vitest run --config vite.config.ts",
"test:watch": "vitest --config vite.config.ts",
"test:e2e": "vitest run --config vite.e2e.config.ts",
"coverage": "vitest run --coverage --config vite.config.ts",
"lint": "eslint --ext ts .",
"start": "node dist/src/index.js",
"build": "tsc --build",
Expand Down
23 changes: 21 additions & 2 deletions sample.env
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
# Single URL or comma-separated list. Each request tries the endpoints in
# order; on a 5xx, 408, 429, network or timeout error the daemon moves to the
# next URL. Other 4xx and validation errors are terminal.
EXECUTION_NODE=http://1.2.3.4:8545
# Optional: when set, includes a Bearer token on every EL request.
# IMPORTANT: the same JWT secret is sent to *every* URL in EXECUTION_NODE.
# If your EL nodes use different jwt.hex files (e.g. Nethermind + Geth),
# you must keep them synced manually, or skip JWT (use unauthenticated RPC).
JWT_SECRET_PATH=/data/neth/jwt.hex
# Single URL or comma-separated list (same fallback semantics as EXECUTION_NODE).
# Voluntary-exit submissions broadcast to every configured CL URL in parallel.
CONSENSUS_NODE=http://1.2.3.4:5051
LOCATOR_ADDRESS=0x1eDf09b5023DC86737b59dE68a8130De878984f5
STAKING_MODULE_ID=1
OPERATOR_ID=123

# Preferred scope config: {"stakingModuleId":[operatorIds]}.
# For multi-module setups:
# EJECTOR_SCOPE={"1":[123],"4":[0,1]}
EJECTOR_SCOPE={"1":[123]}

# Deprecated legacy alternative:
# STAKING_MODULE_ID=1
# OPERATOR_ID=123
# OPERATOR_IDENTIFIERS=[0,1,2]

MESSAGES_LOCATION=messages
MESSAGES_PASSWORD=pass
Expand All @@ -16,6 +33,7 @@ SUBMIT_TX_HASH_ALLOWLIST=[]
EASY_TRACK_ADDRESS=0xF0211b7660680B49De1A7E9f25C65660F0a13Fea

BLOCKS_PRELOAD=50000 # 7 days of blocks
VALIDATORS_BATCH_SIZE=1000

HTTP_PORT=8989
RUN_METRICS=true
Expand All @@ -26,3 +44,4 @@ LOGGER_FORMAT=simple
LOGGER_SECRETS=["MESSAGES_PASSWORD","EXECUTION_NODE", "CONSENSUS_NODE"]

DRY_RUN=false
TRUST_MODE=false
19 changes: 17 additions & 2 deletions sample.infra.env
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
# EXECUTION_NODE and CONSENSUS_NODE accept a single URL or a comma-separated list.
# Each request tries endpoints in order; on a 5xx, 408, 429, network or timeout
# error the daemon moves to the next URL. Other 4xx and validation errors are
# terminal.
# Voluntary-exit submissions broadcast to every configured CL URL in parallel.
EXECUTION_NODE=http://1.2.3.4:8545
CONSENSUS_NODE=http://1.2.3.4:5051
LOCATOR_ADDRESS=0x1eDf09b5023DC86737b59dE68a8130De878984f5
STAKING_MODULE_ID=123
OPERATOR_ID=123

# Preferred scope config: {"stakingModuleId":[operatorIds]}.
# For multi-module setups:
# EJECTOR_SCOPE={"1":[123],"4":[0,1]}
EJECTOR_SCOPE={"123":[123]}

# Deprecated legacy alternative:
# STAKING_MODULE_ID=123
# OPERATOR_ID=123
# OPERATOR_IDENTIFIERS=[0,1,2]

MESSAGES_LOCATION=messages
MESSAGES_PASSWORD=pass
Expand All @@ -15,6 +28,7 @@ SUBMIT_TX_HASH_ALLOWLIST=[]
EASY_TRACK_ADDRESS=0xF0211b7660680B49De1A7E9f25C65660F0a13Fea

BLOCKS_PRELOAD=50000 # 7 days of blocks
VALIDATORS_BATCH_SIZE=1000

HTTP_PORT=8989
RUN_METRICS=true
Expand All @@ -25,3 +39,4 @@ LOGGER_FORMAT=json
LOGGER_SECRETS=["MESSAGES_PASSWORD","EXECUTION_NODE", "CONSENSUS_NODE"]

DRY_RUN=true
TRUST_MODE=false
3 changes: 1 addition & 2 deletions src/app/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { makeLogger } from '../lib/index.js'
import nock from 'nock'
import { makeConfig as mC } from '../services/config/service.js'
import { mockEthServer } from '../test/mock-eth-server.js'
import * as ELMocks from '../services/execution-api/fixtures.js'
import * as CLMocks from '../services/consensus-api/fixtures.js'
Expand All @@ -12,7 +11,7 @@ dotenv.config()
const mockConfig = async (config) => {
const { makeConfig } = (await vi.importActual(
'../services/config/service.js'
)) as { makeConfig: typeof mC }
)) as typeof import('../services/config/service.js')

vi.doMock('../services/config/service.js', () => {
return {
Expand Down
2 changes: 2 additions & 0 deletions src/app/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ExecutionApiService } from '../services/execution-api/service.js'
import type { ConsensusApiService } from '../services/consensus-api/service.js'
import type { AppInfoReaderService } from '../services/app-info-reader/service.js'
import type { MessageReloader } from '../services/message-reloader/message-reloader.js'
import type { ConsistencyChecker } from '../services/consistency/service.js'

export interface Dependencies {
config: ConfigService
Expand All @@ -17,4 +18,5 @@ export interface Dependencies {
executionApi: ExecutionApiService
consensusApi: ConsensusApiService
appInfoReader: AppInfoReaderService
consistencyChecker: ConsistencyChecker
}
8 changes: 8 additions & 0 deletions src/app/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { makeApp } from './service.js'
import { makeMessageReloader } from '../services/message-reloader/message-reloader.js'
import { makeForkVersionResolver } from '../services/fork-version-resolver/service.js'
import { makeExitLogsService } from '../services/exit-logs/service.js'
import { makeConsistencyChecker } from '../services/consistency/service.js'

dotenv.config()

Expand Down Expand Up @@ -74,6 +75,12 @@ export const makeAppModule = async () => {
jwtService
)

const consistencyChecker = makeConsistencyChecker(
executionHttp,
logger,
config
)

const consensusApi = makeConsensusApi(
makeRequest([
retry(3),
Expand Down Expand Up @@ -158,6 +165,7 @@ export const makeAppModule = async () => {
executionApi,
consensusApi,
appInfoReader,
consistencyChecker,
})

return {
Expand Down
Loading
Loading