From 0981e345109ffe69a823396906b618fc5582f10b Mon Sep 17 00:00:00 2001 From: Vladimir Gorkavenko <32727352+vgorkavenko@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:47:17 +0500 Subject: [PATCH] feat: Develop (#96) * chore: Use @clickhouse/client (#86) * fix: case when operator changes his name (#87) * feat: Rewards, missed rewards, penalties (#85) * feat: safe epoch processing (#89) * feat: change CL api timeout policy (#91) * fix: cast to bigint (#93) * chore: remove unused vars. change readme (#95) * chore(deps): bump json5 from 1.0.1 to 1.0.2 (#90) Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump luxon from 1.28.0 to 1.28.1 (#97) Bumps [luxon](https://github.com/moment/luxon) from 1.28.0 to 1.28.1. - [Release notes](https://github.com/moment/luxon/releases) - [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md) - [Commits](https://github.com/moment/luxon/compare/1.28.0...1.28.1) --- updated-dependencies: - dependency-name: luxon dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- README.md | 221 +- docker-compose.yml | 2 +- .../provisioning/dashboards/operators.json | 231 +- .../provisioning/dashboards/rewards.json | 3316 +++++++++++++++++ .../provisioning/dashboards/validators.json | 137 +- package.json | 9 +- src/common/alertmanager/alerts/BasicAlert.ts | 5 +- .../alerts/CriticalMissedAttestations.ts | 16 +- .../alerts/CriticalMissedProposes.ts | 18 +- .../alerts/CriticalNegativeDelta.ts | 18 +- .../alertmanager/alerts/CriticalSlashing.ts | 12 +- .../alertmanager/critical-alerts.module.ts | 7 +- .../alertmanager/critical-alerts.service.ts | 12 +- src/common/config/env.validation.ts | 14 +- .../consensus-provider.service.ts | 148 +- src/common/prometheus/prometheus.constants.ts | 10 + src/common/prometheus/prometheus.service.ts | 76 +- src/duty/attestation/attestation.constants.ts | 76 + src/duty/attestation/attestation.metrics.ts | 42 +- src/duty/attestation/attestation.module.ts | 5 +- src/duty/attestation/attestation.rewards.ts | 58 + src/duty/attestation/attestation.service.ts | 134 +- src/duty/attestation/index.ts | 1 + src/duty/duty.metrics.ts | 18 +- src/duty/duty.module.ts | 15 +- src/duty/duty.rewards.ts | 32 + src/duty/duty.service.ts | 94 +- src/duty/propose/index.ts | 1 + src/duty/propose/propose.constants.ts | 9 + src/duty/propose/propose.metrics.ts | 10 +- src/duty/propose/propose.module.ts | 5 +- src/duty/propose/propose.rewards.ts | 75 + src/duty/state/state.metrics.ts | 54 +- src/duty/state/state.service.ts | 40 +- src/duty/summary/summary.metrics.ts | 90 +- src/duty/summary/summary.module.ts | 4 +- src/duty/summary/summary.service.ts | 80 +- src/duty/sync/index.ts | 1 + src/duty/sync/sync.constants.ts | 13 + src/duty/sync/sync.metrics.ts | 17 +- src/duty/sync/sync.module.ts | 5 +- src/duty/sync/sync.rewards.ts | 34 + src/duty/sync/sync.service.ts | 26 +- src/inspector/inspector.service.ts | 91 +- .../clickhouse/clickhouse.constants.ts | 450 ++- src/storage/clickhouse/clickhouse.service.ts | 448 ++- src/storage/clickhouse/clickhouse.types.ts | 53 +- .../migrations/migration_000002_rewards.ts | 19 + .../migrations/migration_000003_epoch_meta.ts | 18 + .../migration_000004_epoch_processing.ts | 10 + test/duties.e2e-spec.ts | 61 +- yarn.lock | 349 +- 52 files changed, 5497 insertions(+), 1193 deletions(-) create mode 100644 docker/grafana/provisioning/dashboards/rewards.json create mode 100644 src/duty/attestation/attestation.constants.ts create mode 100644 src/duty/attestation/attestation.rewards.ts create mode 100644 src/duty/duty.rewards.ts create mode 100644 src/duty/propose/propose.constants.ts create mode 100644 src/duty/propose/propose.rewards.ts create mode 100644 src/duty/sync/sync.constants.ts create mode 100644 src/duty/sync/sync.rewards.ts create mode 100644 src/storage/clickhouse/migrations/migration_000002_rewards.ts create mode 100644 src/storage/clickhouse/migrations/migration_000003_epoch_meta.ts create mode 100644 src/storage/clickhouse/migrations/migration_000004_epoch_processing.ts diff --git a/README.md b/README.md index 36426d8d..0799257f 100644 --- a/README.md +++ b/README.md @@ -65,43 +65,122 @@ By default, monitoring bot fetches validator keys from Lido contract, but you ca If you want to implement your own source, it must match [RegistrySource interface](src/common/validators-registry/registry-source.interface.ts) and be included in [RegistryModule providers](src/common/validators-registry/registry.module.ts) ## Application Env variables -| **Variable** | **Description** | **Required** | **Default** | -|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------|-----------------------------------------| -| LOG_LEVEL | Balval log level | | | -| LOG_FORMAT | Balval log format (simple or json) | | | -| DB_HOST | Clickhouse server host | true | | -| DB_USER | Clickhouse server user | true | | -| DB_PASSWORD | Clickhouse server password | true | | -| DB_NAME | Clickhouse server name | true | | -| DB_PORT | Clickhouse server port | false | 8123 | -| HTTP_PORT | Port for Prometheus HTTP server in Balval | false | 8080 | -| DB_MAX_RETRIES | Max retries for each query to DB | false | 10 | -| DB_MIN_BACKOFF_SEC | Min backoff for DB query retrier | false | 1 | -| DB_MAX_BACKOFF_SEC | Max backoff for DB query retrier | false | 120 | -| LOG_LEVEL | Logging level | false | info | -| DRY_RUN | Option to run Balval in dry mode. This means that Balval runs a main cycle once every 24 hours | false | false | -| ETH_NETWORK | Ethereum network ID for connection execution layer RPC | true | | -| EL_RPC_URLS | Ethereum execution layer comma separated RPC urls | true | | -| CL_API_URLS | Ethereum consensus layer comma separated API urls | true | | -| CL_API_RETRY_DELAY_MS | Ethereum consensus layer request retry delay | false | 500 | -| CL_API_GET_RESPONSE_TIMEOUT | Ethereum consensus layer GET response (header) timeout | false | 15 * 1000 | -| CL_API_POST_RESPONSE_TIMEOUT | Ethereum consensus layer POST response (header) timeout | false | 15 * 1000 | -| CL_API_POST_REQUEST_CHUNK_SIZE | Ethereum consensus layer data chunk size for large POST requests | false | 30000 | -| CL_API_MAX_RETRIES | Ethereum consensus layer max retries for all requests | false | 1 | -| CL_API_GET_BLOCK_INFO_MAX_RETRIES | Ethereum consensus layer max retries for fetching block info. Independent of `CL_API_MAX_RETRIES` | false | 1 | -| FETCH_INTERVAL_SLOTS | Count of slots in Ethereum consensus layer epoch | false | 32 | -| CHAIN_SLOT_TIME_SECONDS | Ethereum consensus layer time slot size | false | 12 | -| START_EPOCH | Ethereum consensus layer epoch for start Balval | false | 155000 | -| VALIDATOR_REGISTRY_SOURCE | Validators registry source. Possible values: lido (Lido contract), file | false | lido | -| VALIDATOR_REGISTRY_FILE_SOURCE_PATH | Validators registry file source path. It makes sense to change default value if you set `VALIDATOR_REGISTRY_SOURCE` to `file` | false | ./docker/validators/custom_mainnet.yaml | -| VALIDATOR_REGISTRY_LIDO_SOURCE_SQLITE_CACHE_PATH | Validators registry lido source sqlite cache path. It makes sense to change default value if you set `VALIDATOR_REGISTRY_SOURCE` to `lido` | false | ./docker/validators/lido_mainnet.db | -| SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG | Distance (down) from Blockchain Sync Participation average after which we think that our sync participation is bad | false | 0 | -| SYNC_PARTICIPATION_EPOCHS_LESS_THAN_CHAIN_AVG | Number epochs after which we think that our sync participation is bad and alert about that | false | 3 | -| BAD_ATTESTATION_EPOCHS | Number epochs after which we think that our attestation is bad and alert about that | false | 3 | -| CRITICAL_ALERTS_ALERTMANAGER_URL | If passed, Balval sends additional critical alerts about validators performance to Alertmanager | false | | -| CRITICAL_ALERTS_MIN_VAL_COUNT | Critical alerts will be sent for Node Operators with validators count greater this value | false | | -| CRITICAL_ALERTS_ALERTMANAGER_LABELS | Additional labels for critical alerts. Must be in JSON string format.For example - '{"a":"valueA","b":"valueB"}' | false | | +--- +`LOG_LEVEL` - Application log level +* **Required:** false +* **Default:** info +--- +`LOG_FORMAT` - Application log format (simple or json) +* **Required:** false +* **Default:** json +--- +`DB_HOST` - Clickhouse server host +* **Required:** true +--- +`DB_USER` - Clickhouse server user +* **Required:** true +--- +`DB_PASSWORD` - Clickhouse server password +* **Required:** true +--- +`DB_NAME` - Clickhouse server DB name +* **Required:** true +--- +`DB_PORT` - Clickhouse server port +* **Required:** false +* **Default:** 8123 +--- +`HTTP_PORT` - Port for Prometheus HTTP server in application +* **Required:** false +* **Default:** 8080 +--- +`DB_MAX_RETRIES` - Max retries for each query to DB +* **Required:** false +* **Default:** 10 +--- +`DB_MIN_BACKOFF_SEC` - Min backoff for DB query retrier +* **Required:** false +* **Default:** 1 +--- +`DB_MAX_BACKOFF_SEC` - Min backoff for DB query retrier +* **Required:** false +* **Default:** 120 +--- +`DRY_RUN` - Run application in dry mode. This means that it runs a main cycle once every 24 hours +* **Required:** false +* **Default:** false +--- +`ETH_NETWORK` - Ethereum network ID for connection execution layer RPC +* **Required:** true +--- +`EL_RPC_URLS` - Ethereum execution layer comma separated RPC urls +* **Required:** true +--- +`CL_API_URLS` - Ethereum consensus layer comma separated API urls +* **Required:** true +--- +`CL_API_RETRY_DELAY_MS` - Ethereum consensus layer request retry delay +* **Required:** false +* **Default:** 500 +--- +`CL_API_GET_RESPONSE_TIMEOUT` - Ethereum consensus layer GET response (header) timeout (ms) +* **Required:** false +* **Default:** 15000 +--- +`CL_API_MAX_RETRIES` - Ethereum consensus layer max retries for all requests +* **Required:** false +* **Default:** 1 (means that request will be executed once) +--- +`CL_API_GET_BLOCK_INFO_MAX_RETRIES` - Ethereum consensus layer max retries for fetching block info. Independent of `CL_API_MAX_RETRIES` +* **Required:** false +* **Default:** 1 (means that request will be executed once) +--- +`FETCH_INTERVAL_SLOTS` - Count of slots in Ethereum consensus layer epoch +* **Required:** false +* **Default:** 32 +--- +`CHAIN_SLOT_TIME_SECONDS` - Ethereum consensus layer time slot size (sec) +* **Required:** false +* **Default:** 12 +--- +`START_EPOCH` - Ethereum consensus layer epoch for start application +* **Required:** false +* **Default:** 155000 +--- +`VALIDATOR_REGISTRY_SOURCE` - Validators registry source. Possible values: `lido` (Lido contract), `file` +* **Required:** false +* **Default:** lido +--- +`VALIDATOR_REGISTRY_FILE_SOURCE_PATH` - Validators registry file source path. It makes sense to change default value if you set `VALIDATOR_REGISTRY_SOURCE` to `file` +* **Required:** false +* **Default:** ./docker/validators/custom_mainnet.yaml +--- +`VALIDATOR_REGISTRY_LIDO_SOURCE_SQLITE_CACHE_PATH` - Validators registry lido source sqlite cache path. It makes sense to change default value if you set `VALIDATOR_REGISTRY_SOURCE` to `lido` +* **Required:** false +* **Default:** ./docker/validators/lido_mainnet.db +--- +`SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG` - Distance (down) from Blockchain Sync Participation average after which we think that our sync participation is bad +* **Required:** false +* **Default:** 0 +--- +`SYNC_PARTICIPATION_EPOCHS_LESS_THAN_CHAIN_AVG` - Number epochs after which we think that our sync participation is bad and alert about that +* **Required:** false +* **Default:** 3 +--- +`BAD_ATTESTATION_EPOCHS` - Number epochs after which we think that our attestation is bad and alert about that +* **Required:** false +* **Default:** 3 +--- +`CRITICAL_ALERTS_ALERTMANAGER_URL` - If passed, application sends additional critical alerts about validators performance to Alertmanager +* **Required:** false +--- +`CRITICAL_ALERTS_MIN_VAL_COUNT` - Critical alerts will be sent for Node Operators with validators count greater this value +* **Required:** false +--- +`CRITICAL_ALERTS_ALERTMANAGER_LABELS` - Additional labels for critical alerts. Must be in JSON string format. Example - '{"a":"valueA","b":"valueB"}' +* **Required:** false +--- ## Application critical alerts (via Alertmanager) @@ -121,35 +200,47 @@ And if `ethereum_validators_monitoring_data_actuality < 1h` it allows you to rec ## Application metrics -| Metric | Labels | Description | -|----------------------------------------------------------------------------------------------------------|----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ethereum_validators_monitoring_validators | owner, status | Count of validators in chain | -| ethereum_validators_monitoring_user_validators | nos_name, status | Count of validators for each user Node Operator | -| ethereum_validators_monitoring_data_actuality | | Application data actuality in ms | -| ethereum_validators_monitoring_fetch_interval | | The same as `FETCH_INTERVAL_SLOTS` | -| ethereum_validators_monitoring_sync_participation_distance_down_from_chain_avg | | The same as `SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG` | -| ethereum_validators_monitoring_epoch_number | | Current epoch number in app work process | -| ethereum_validators_monitoring_contract_keys_total | | Total user validators keys | -| ethereum_validators_monitoring_steth_buffered_ether_total | | Buffered Ether (ETH) in Lido contract | -| ethereum_validators_monitoring_total_balance_24h_difference | | Total user validators balance difference (24 hours) | -| ethereum_validators_monitoring_validator_balances_delta | nos_name | Validators balance delta for each user Node Operator | -| ethereum_validators_monitoring_validator_quantile_001_balances_delta | nos_name | Validators 0.1% quantile balances delta for each user Node Operator | -| ethereum_validators_monitoring_validator_count_with_negative_balances_delta | nos_name | Number of validators with negative balances delta for each user Node Operator | -| ethereum_validators_monitoring_validator_count_with_sync_participation_less_avg | nos_name | Number of validators with sync committee participation less avg for each user Node Operator | -| ethereum_validators_monitoring_validator_count_miss_attestation | nos_name | Number of validators miss attestation for each user Node Operator | -| ethereum_validators_monitoring_validator_count_invalid_attestation | nos_name, reason | Number of validators with invalid properties (head, target, source) \ high inc. delay in attestation for each user Node Operator | -| ethereum_validators_monitoring_validator_count_invalid_attestation_last_n_epoch | nos_name, reason, epoch_interval | Number of validators with invalid properties (head, target, source) \ high inc. delay in attestation last `BAD_ATTESTATION_EPOCHS` epoch for each user Node Operator | -| ethereum_validators_monitoring_validator_count_miss_attestation_last_n_epoch | nos_name, epoch_interval | Number of validators miss attestation last `BAD_ATTESTATION_EPOCHS` epoch for each user Node Operator | -| ethereum_validators_monitoring_validator_count_high_avg_inc_delay_of_n_epoch | nos_name, epoch_interval | Number of validators with high avg inc. delay of N epochs for each user Node Operator | -| ethereum_validators_monitoring_validator_count_high_inc_delay_last_n_epoch | nos_name, epoch_interval | Number of validators with inc. delay > 2 last N epochs for each user Node Operator | -| ethereum_validators_monitoring_validator_count_invalid_attestation_property_last_n_epoch | nos_name, epoch_interval | Number of validators with two invalid attestation property (head or target or source) last N epochs for each user Node Operator | -| ethereum_validators_monitoring_high_reward_validator_count_miss_attestation_last_n_epoch | nos_name, epoch_interval | Number of validators miss attestation last `BAD_ATTESTATION_EPOCHS` epoch (with possible high reward in the future) for each user Node Operator | -| ethereum_validators_monitoring_validator_count_with_sync_participation_less_avg_last_n_epoch | nos_name, epoch_interval | Number of validators with sync participation less than avg last `SYNC_PARTICIPATION_EPOCHS_LESS_THAN_CHAIN_AVG` epoch for each user Node Operator | -| ethereum_validators_monitoring_high_reward_validator_count_with_sync_participation_less_avg_last_n_epoch | nos_name, epoch_interval | Number of validators with sync participation less than avg last `SYNC_PARTICIPATION_EPOCHS_LESS_THAN_CHAIN_AVG` epoch (with possible high reward in the future) for each user Node Operator | -| ethereum_validators_monitoring_validator_count_miss_propose | nos_name | Number of validators miss propose for each user Node Operator | -| ethereum_validators_monitoring_high_reward_validator_count_miss_propose | nos_name | Number of validators miss propose (with possible high reward in the future) | -| ethereum_validators_monitoring_user_sync_participation_avg_percent | | User sync committee validators participation avg percent | -| ethereum_validators_monitoring_chain_sync_participation_avg_percent | | All sync committee validators participation avg percent | +**WARNING: all metrics are prefixed with `ethereum_validators_monitoring_`** + +| Metric | Labels | Description | +|---------------------------------------------------------------------------|----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| validators | owner, status | Count of validators in chain | +| user_validators | nos_name, status | Count of validators for each user Node Operator | +| data_actuality | | Application data actuality in ms | +| fetch_interval | | The same as `FETCH_INTERVAL_SLOTS` | +| sync_participation_distance_down_from_chain_avg | | The same as `SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG` | +| epoch_number | | Current epoch number in app work process | +| contract_keys_total | | Total user validators keys | +| steth_buffered_ether_total | | Buffered Ether (ETH) in Lido contract | +| total_balance_24h_difference | | Total user validators balance difference (24 hours) | +| validator_balances_delta | nos_name | Validators balance delta for each user Node Operator | +| validator_quantile_001_balances_delta | nos_name | Validators 0.1% quantile balances delta for each user Node Operator | +| validator_count_with_negative_balances_delta | nos_name | Number of validators with negative balances delta for each user Node Operator | +| validator_count_with_sync_participation_less_avg | nos_name | Number of validators with sync committee participation less avg for each user Node Operator | +| validator_count_miss_attestation | nos_name | Number of validators miss attestation for each user Node Operator | +| validator_count_invalid_attestation | nos_name, reason | Number of validators with invalid properties (head, target, source) \ high inc. delay in attestation for each user Node Operator | +| validator_count_invalid_attestation_last_n_epoch | nos_name, reason, epoch_interval | Number of validators with invalid properties (head, target, source) \ high inc. delay in attestation last `BAD_ATTESTATION_EPOCHS` epoch for each user Node Operator | +| validator_count_miss_attestation_last_n_epoch | nos_name, epoch_interval | Number of validators miss attestation last `BAD_ATTESTATION_EPOCHS` epoch for each user Node Operator | +| validator_count_high_avg_inc_delay_of_n_epoch | nos_name, epoch_interval | Number of validators with high avg inc. delay of N epochs for each user Node Operator | +| validator_count_high_inc_delay_last_n_epoch | nos_name, epoch_interval | Number of validators with inc. delay > 2 last N epochs for each user Node Operator | +| validator_count_invalid_attestation_property_last_n_epoch | nos_name, epoch_interval | Number of validators with two invalid attestation property (head or target or source) last N epochs for each user Node Operator | +| high_reward_validator_count_miss_attestation_last_n_epoch | nos_name, epoch_interval | Number of validators miss attestation last `BAD_ATTESTATION_EPOCHS` epoch (with possible high reward in the future) for each user Node Operator | +| validator_count_with_sync_participation_less_avg_last_n_epoch | nos_name, epoch_interval | Number of validators with sync participation less than avg last `SYNC_PARTICIPATION_EPOCHS_LESS_THAN_CHAIN_AVG` epoch for each user Node Operator | +| high_reward_validator_count_with_sync_participation_less_avg_last_n_epoch | nos_name, epoch_interval | Number of validators with sync participation less than avg last `SYNC_PARTICIPATION_EPOCHS_LESS_THAN_CHAIN_AVG` epoch (with possible high reward in the future) for each user Node Operator | +| validator_count_miss_propose | nos_name | Number of validators miss propose for each user Node Operator | +| high_reward_validator_count_miss_propose | nos_name | Number of validators miss propose (with possible high reward in the future) | +| user_sync_participation_avg_percent | | User sync committee validators participation avg percent | +| chain_sync_participation_avg_percent | | All sync committee validators participation avg percent | +| operator_real_balance_delta | nos_name | Real operator balance change. Between N and N-1 epochs | +| operator_calculated_balance_delta | nos_name | Calculated operator balance change based on rewards calculation | +| operator_calculated_balance_calculation_error | nos_name | Diff between calculated and real balance change | +| avg_chain_reward | duty | Average validator's reward for each duty | +| operator_reward | nos_name, duty | Operator's reward for each duty | +| avg_chain_missed_reward | duty | Average validator's missed reward for each duty | +| operator_missed_reward | nos_name, duty | Operator's missed reward for each duty | +| avg_chain_penalty | duty | Average validator's penalty for each duty | +| operator_penalty | nos_name, duty | Operator's penalty for each duty | + ## Release flow diff --git a/docker-compose.yml b/docker-compose.yml index 93bf41b2..7703ae00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,7 +105,7 @@ services: - "DISCORD_WEBHOOK=${DISCORD_WEBHOOK_URL}" grafana: - image: grafana/grafana:latest + image: grafana/grafana:8.5.15 sysctls: - net.ipv6.conf.lo.disable_ipv6=0 - net.ipv6.conf.all.disable_ipv6=0 diff --git a/docker/grafana/provisioning/dashboards/operators.json b/docker/grafana/provisioning/dashboards/operators.json index ede14a1a..972c2df2 100644 --- a/docker/grafana/provisioning/dashboards/operators.json +++ b/docker/grafana/provisioning/dashboards/operators.json @@ -24,7 +24,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "iteration": 1668942230709, + "iteration": 1672296019449, "links": [ { "asDropdown": false, @@ -117,7 +117,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -183,7 +183,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -264,7 +264,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -328,7 +328,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -399,7 +399,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -461,7 +461,7 @@ "text": {}, "textMode": "name" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -932,7 +932,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -944,8 +944,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_name = '${nos_name_var}' AND\n epoch = ${epoch_number_var}\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_name = '${nos_name_var}' AND\n epoch = ${epoch_number_var} - 6\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nHAVING (current.val_balance - previous.val_balance) < 0\nORDER BY GWei ASC\nLIMIT ${limit}", - "rawQuery": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_name = 'EU Nordic & East' AND\n epoch = 161310\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_name = 'EU Nordic & East' AND\n epoch = 161310 - 6\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nHAVING (current.val_balance - previous.val_balance) < 0\nORDER BY GWei ASC\nLIMIT 100", + "query": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_name = '${nos_name_var}' AND\n epoch = ${epoch_number_var}\n LIMIT 1 by val_id\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_name = '${nos_name_var}' AND\n epoch = ${epoch_number_var} - 6\n LIMIT 1 by val_id\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nHAVING (current.val_balance - previous.val_balance) < 0\nORDER BY GWei ASC\nLIMIT ${limit}", + "rawQuery": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_name = 'Allnodes' AND\n epoch = 170498\n LIMIT 1 by val_id\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_name = 'Allnodes' AND\n epoch = 170498 - 6\n LIMIT 1 by val_id\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nHAVING (current.val_balance - previous.val_balance) < 0\nORDER BY GWei ASC\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true @@ -1053,7 +1053,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -1065,8 +1065,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n block_to_propose as Block,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND block_proposed = 0 AND (epoch = ${epoch_number_var}) AND val_nos_name = '${nos_name_var}'\nORDER BY block_to_propose DESC, val_id", - "rawQuery": "SELECT\n block_to_propose as Block,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND block_proposed = 0 AND (epoch = 161310) AND val_nos_name = 'EU Nordic & East'\nORDER BY block_to_propose DESC, val_id", + "query": "SELECT\n block_to_propose as Block,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND block_proposed = 0 AND (epoch = ${epoch_number_var}) AND val_nos_name = '${nos_name_var}'\nORDER BY block_to_propose DESC, val_id\nLIMIT 1 by val_id", + "rawQuery": "SELECT\n block_to_propose as Block,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND block_proposed = 0 AND (epoch = 170499) AND val_nos_name = 'Allnodes'\nORDER BY block_to_propose DESC, val_id\nLIMIT 1 by val_id", "refId": "A", "round": "0s", "skip_comments": true @@ -1186,7 +1186,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -1199,8 +1199,8 @@ "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "hide": false, "intervalFactor": 1, - "query": "SELECT\n val_id as Validator\nFROM (\n SELECT\n val_id,\n count() AS count_fail\n FROM\n validators_summary\n WHERE\n is_sync = 1 AND sync_percent < (${chain_sync_avg_participation} - ${sync_participation_distance_var}) AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${sync_epochs_var})) AND\n val_nos_name = '${nos_name_var}'\n GROUP BY val_id\n)\nWHERE count_fail = ${sync_epochs_var}\nORDER BY count_fail DESC, val_id", - "rawQuery": "SELECT\n val_id as Validator\nFROM (\n SELECT\n val_id,\n count() AS count_fail\n FROM\n validators_summary\n WHERE\n is_sync = 1 AND sync_percent < (97.296142578125 - 10) AND\n (epoch <= 161310 AND epoch > (161310 - 3)) AND\n val_nos_name = 'EU Nordic & East'\n GROUP BY val_id\n)\nWHERE count_fail = 3\nORDER BY count_fail DESC, val_id", + "query": "SELECT\n val_id as Validator\nFROM (\n SELECT\n val_id,\n count() AS count_fail\n FROM (\n SELECT val_id\n FROM validators_summary\n WHERE\n is_sync = 1 AND sync_percent < (${chain_sync_avg_participation} - ${sync_participation_distance_var}) AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${sync_epochs_var})) AND\n val_nos_name = '${nos_name_var}'\n LIMIT 1 by epoch, val_id \n )\n GROUP BY val_id\n)\nWHERE count_fail = ${sync_epochs_var}\nORDER BY count_fail DESC, val_id", + "rawQuery": "SELECT\n val_id as Validator\nFROM (\n SELECT\n val_id,\n count() AS count_fail\n FROM (\n SELECT val_id\n FROM validators_summary\n WHERE\n is_sync = 1 AND sync_percent < (98.48790320474654 - 0) AND\n (epoch <= 170500 AND epoch > (170500 - 3)) AND\n val_nos_name = 'Allnodes'\n LIMIT 1 by epoch, val_id \n )\n GROUP BY val_id\n)\nWHERE count_fail = 3\nORDER BY count_fail DESC, val_id", "refId": "B", "round": "0s", "skip_comments": true @@ -1287,7 +1287,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -1299,8 +1299,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\nSELECT\n val_id as Validator\nFROM (\n SELECT\n val_id,\n count() as count_fail\n FROM validators_summary\n WHERE att_happened = 0 AND \n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${att_epochs_var})) AND\n val_nos_name = '${nos_name_var}'\n GROUP BY val_id\n)\nWHERE count_fail = ${att_epochs_var}\nORDER BY count_fail DESC, val_id\nLIMIT ${limit}", - "rawQuery": "SELECT\n val_id as Validator\nFROM (\n SELECT\n val_id,\n count() as count_fail\n FROM validators_summary\n WHERE att_happened = 0 AND \n (epoch <= 161309 AND epoch > (161309 - 3)) AND\n val_nos_name = 'EU Nordic & East'\n GROUP BY val_id\n)\nWHERE count_fail = 3\nORDER BY count_fail DESC, val_id\nLIMIT 100", + "query": "\nSELECT\n val_id as Validator\nFROM (\n SELECT\n val_id,\n count() as count_fail\n FROM (\n SELECT val_id\n FROM validators_summary\n WHERE \n att_happened = 0 AND val_nos_name = '${nos_name_var}' AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${att_epochs_var}))\n LIMIT 1 by epoch, val_id\n )\n GROUP BY val_id\n)\nWHERE count_fail = ${att_epochs_var}\nORDER BY count_fail DESC, val_id\nLIMIT ${limit}", + "rawQuery": "SELECT\n val_id as Validator\nFROM (\n SELECT\n val_id,\n count() as count_fail\n FROM (\n SELECT val_id\n FROM validators_summary\n WHERE \n att_happened = 0 AND val_nos_name = 'Allnodes' AND\n (epoch <= 170499 AND epoch > (170499 - 3))\n LIMIT 1 by epoch, val_id\n )\n GROUP BY val_id\n)\nWHERE count_fail = 3\nORDER BY count_fail DESC, val_id\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true @@ -1399,7 +1399,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -1411,8 +1411,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n val_id as Validator,\n count() as count_fail\nFROM validators_summary\nWHERE\n val_nos_name = '${nos_name_var}' AND\n att_happened = 1 AND\n att_inc_delay > 1 AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${att_epochs_var}))\nGROUP BY val_id, val_nos_name\nHAVING count_fail = ${att_epochs_var}\nORDER BY count_fail DESC, val_id\nLIMIT ${limit}", - "rawQuery": "SELECT\n val_id as Validator,\n count() as count_fail\nFROM validators_summary\nWHERE\n val_nos_name = 'EU Nordic & East' AND\n att_happened = 1 AND\n att_inc_delay > 1 AND\n (epoch <= 161310 AND epoch > (161310 - 3))\nGROUP BY val_id, val_nos_name\nHAVING count_fail = 3\nORDER BY count_fail DESC, val_id\nLIMIT 100", + "query": "SELECT\n val_id as Validator,\n count() as count_fail\nFROM (\n SELECT val_id\n FROM validators_summary\n WHERE\n val_nos_name = '${nos_name_var}' AND\n att_happened = 1 AND\n att_inc_delay > 1 AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${att_epochs_var}))\n LIMIT 1 by epoch, val_id\n)\nGROUP BY val_id\nHAVING count_fail = ${att_epochs_var}\nORDER BY count_fail DESC, val_id\nLIMIT ${limit}", + "rawQuery": "SELECT\n val_id as Validator,\n count() as count_fail\nFROM (\n SELECT val_id\n FROM validators_summary\n WHERE\n val_nos_name = 'Allnodes' AND\n att_happened = 1 AND\n att_inc_delay > 1 AND\n (epoch <= 170500 AND epoch > (170500 - 3))\n LIMIT 1 by epoch, val_id\n)\nGROUP BY val_id\nHAVING count_fail = 3\nORDER BY count_fail DESC, val_id\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true @@ -1512,7 +1512,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -1524,8 +1524,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n val_id as Validator,\n count() as count_fail\nFROM validators_summary\nWHERE\n val_nos_name = '${nos_name_var}' AND\n (att_valid_head + att_valid_target + att_valid_source = 1) AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${att_epochs_var}))\nGROUP BY val_id, val_nos_name\nHAVING count_fail = ${att_epochs_var}\nORDER BY count_fail DESC, val_id\nLIMIT ${limit}", - "rawQuery": "SELECT\n val_id as Validator,\n count() as count_fail\nFROM validators_summary\nWHERE\n val_nos_name = 'EU Nordic & East' AND\n (att_valid_head + att_valid_target + att_valid_source = 1) AND\n (epoch <= 161310 AND epoch > (161310 - 3))\nGROUP BY val_id, val_nos_name\nHAVING count_fail = 3\nORDER BY count_fail DESC, val_id\nLIMIT 100", + "query": "SELECT\n val_id as Validator,\n count() as count_fail\nFROM (\n SELECT val_id\n FROM validators_summary\n WHERE\n val_nos_name = '${nos_name_var}' AND\n (att_valid_head + att_valid_target + att_valid_source = 1) AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${att_epochs_var}))\n LIMIT 1 by epoch, val_id\n)\nGROUP BY val_id\nHAVING count_fail = ${att_epochs_var}\nORDER BY count_fail DESC, val_id\nLIMIT ${limit}", + "rawQuery": "SELECT\n val_id as Validator,\n count() as count_fail\nFROM (\n SELECT val_id\n FROM validators_summary\n WHERE\n val_nos_name = 'Allnodes' AND\n (att_valid_head + att_valid_target + att_valid_source = 1) AND\n (epoch <= 170500 AND epoch > (170500 - 3))\n LIMIT 1 by epoch, val_id\n)\nGROUP BY val_id\nHAVING count_fail = 3\nORDER BY count_fail DESC, val_id\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true @@ -1612,7 +1612,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -1624,8 +1624,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n val_status as Status,\n val_id as Validator\nFROM validators_summary\nWHERE (val_status == 'active_slashed' OR val_status == 'exited_slashed' OR val_slashed == 1) and val_nos_name = '${nos_name_var}' and epoch = ${epoch_number_var}\nLIMIT ${limit}", - "rawQuery": "SELECT\n val_status as Status,\n val_id as Validator\nFROM validators_summary\nWHERE (val_status == 'active_slashed' OR val_status == 'exited_slashed' OR val_slashed == 1) and val_nos_name = 'EU Nordic & East' and epoch = 161310\nLIMIT 100", + "query": "SELECT\n val_status as Status,\n val_id as Validator\nFROM validators_summary\nWHERE (val_status == 'active_slashed' OR val_status == 'exited_slashed' OR val_slashed == 1) and val_nos_name = '${nos_name_var}' and epoch = ${epoch_number_var}\nLIMIT 1 by val_id", + "rawQuery": "SELECT\n val_status as Status,\n val_id as Validator\nFROM validators_summary\nWHERE (val_status == 'active_slashed' OR val_status == 'exited_slashed' OR val_slashed == 1) and val_nos_name = 'Allnodes' and epoch = 170499\nLIMIT 1 by val_id", "refId": "A", "round": "0s", "skip_comments": true @@ -1696,7 +1696,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] }, @@ -1802,7 +1803,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null }, { "color": "transparent", @@ -1896,7 +1898,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null }, { "color": "transparent", @@ -1964,7 +1967,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -2038,10 +2042,10 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { - "datasource": { + "datasource": { "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, @@ -2050,8 +2054,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = ${epoch_number_var}\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = ${epoch_number_var} - 6\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nORDER BY GWei ASC\nLIMIT 100", - "rawQuery": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = 161308\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = 161308 - 6\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nORDER BY GWei ASC\nLIMIT 100", + "query": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = ${epoch_number_var}\n LIMIT 1 by val_id\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = ${epoch_number_var} - 6\n LIMIT 1 by val_id\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nORDER BY GWei ASC\nLIMIT 100", + "rawQuery": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = 170500\n LIMIT 1 by val_id\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = 170500 - 6\n LIMIT 1 by val_id\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nORDER BY GWei ASC\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true @@ -2108,7 +2112,8 @@ "mode": "absolute", "steps": [ { - "color": "dark-red" + "color": "dark-red", + "value": null } ] }, @@ -2193,7 +2198,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -2269,10 +2275,10 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { - "datasource": { + "datasource": { "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, @@ -2281,8 +2287,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n block_to_propose as Block,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND val_nos_name = '${nos_name_var}' AND block_proposed = 0 AND (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\nORDER BY block_to_propose DESC, val_id", - "rawQuery": "SELECT\n block_to_propose as Block,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND val_nos_name = 'EU Nordic & East' AND block_proposed = 0 AND (epoch <= 161308 AND epoch > (161308 - 48))\nORDER BY block_to_propose DESC, val_id", + "query": "SELECT\n block_to_propose as Block,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND val_nos_name = '${nos_name_var}' AND block_proposed = 0 AND (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\nORDER BY block_to_propose DESC, val_id\nLIMIT 1 by epoch, val_id", + "rawQuery": "SELECT\n block_to_propose as Block,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND val_nos_name = 'Allnodes' AND block_proposed = 0 AND (epoch <= 170500 AND epoch > (170500 - 9))\nORDER BY block_to_propose DESC, val_id\nLIMIT 1 by epoch, val_id", "refId": "A", "round": "0s", "skip_comments": true @@ -2336,7 +2342,8 @@ "mode": "absolute", "steps": [ { - "color": "semi-dark-green" + "color": "semi-dark-green", + "value": null } ] } @@ -2555,13 +2562,11 @@ "type": "prometheus", "uid": "PBFA97CFB590B2093" }, - "editorMode": "code", "exemplar": true, "expr": "sum(ethereum_validators_monitoring_other_sync_participation_avg_percent) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", "hide": false, "interval": "", "legendFormat": "Rest of chain", - "range": true, "refId": "A" }, { @@ -2569,13 +2574,11 @@ "type": "prometheus", "uid": "PBFA97CFB590B2093" }, - "editorMode": "code", "exemplar": true, "expr": "avg(ethereum_validators_monitoring_operator_sync_participation_avg_percent{nos_name!='${nos_name_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", "hide": false, "interval": "", - "legendFormat": "Rest of user's", - "range": true, + "legendFormat": "Rest of User's", "refId": "B" }, { @@ -2726,10 +2729,10 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { - "datasource": { + "datasource": { "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, @@ -2738,8 +2741,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n sync_percent as Percent,\n val_id as Validator\nFROM\n validators_summary\nWHERE is_sync = 1 AND val_nos_name = '${nos_name_var}' AND epoch = ${epoch_number_var} AND sync_percent < (${chain_sync_avg_participation} - ${sync_participation_distance_var})\nORDER BY sync_percent", - "rawQuery": "SELECT\n sync_percent as Percent,\n val_id as Validator\nFROM\n validators_summary\nWHERE is_sync = 1 AND val_nos_name = 'EU Nordic & East' AND epoch = 161308 AND sync_percent < (97.222900390625 - 10)\nORDER BY sync_percent", + "query": "SELECT\n sync_percent as Percent,\n val_id as Validator\nFROM\n validators_summary\nWHERE is_sync = 1 AND val_nos_name = '${nos_name_var}' AND epoch = ${epoch_number_var} AND sync_percent < (${chain_sync_avg_participation} - ${sync_participation_distance_var})\nORDER BY sync_percent\nLIMIT 1 by val_id", + "rawQuery": "SELECT\n sync_percent as Percent,\n val_id as Validator\nFROM\n validators_summary\nWHERE is_sync = 1 AND val_nos_name = 'Allnodes' AND epoch = 170501 AND sync_percent < (95.4833984375 - 0)\nORDER BY sync_percent\nLIMIT 1 by val_id", "refId": "A", "round": "0s", "skip_comments": true @@ -2930,7 +2933,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3004,10 +3008,10 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { - "datasource": { + "datasource": { "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, @@ -3016,14 +3020,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": " SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_happened = 0 AND val_nos_name = '${nos_name_var}' AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT ${limit}", - "rawQuery": "SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_happened = 0 AND val_nos_name = 'EU Nordic & East' AND\n (epoch <= 161309 AND epoch > (161309 - 5))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT 100", + "query": " SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_happened = 0 AND val_nos_name = '${nos_name_var}' AND epoch = ${epoch_number_var}\n ORDER BY val_id\n LIMIT 1 by val_id\n LIMIT ${limit}", + "rawQuery": "SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_happened = 0 AND val_nos_name = 'Allnodes' AND epoch = 170501\n ORDER BY val_id\n LIMIT 1 by val_id\n LIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of MISSED attestations (${epochs_range} epochs range)", + "title": "Validators with MISSED attestation", "type": "table" }, { @@ -3071,7 +3075,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3090,8 +3095,8 @@ "calcs": [ "lastNotNull" ], - "displayMode": "hidden", - "placement": "right", + "displayMode": "list", + "placement": "bottom", "sortBy": "Last *", "sortDesc": true }, @@ -3122,7 +3127,7 @@ "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, - "description": "inc. delay > 2", + "description": "inc. delay > 1", "fieldConfig": { "defaults": { "color": { @@ -3174,7 +3179,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3248,10 +3254,10 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { - "datasource": { + "datasource": { "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, @@ -3260,14 +3266,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\n SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_happened = 1 AND att_inc_delay > 1 AND val_nos_name = '${nos_name_var}' AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT ${limit}", - "rawQuery": "SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_happened = 1 AND att_inc_delay > 1 AND val_nos_name = 'EU Nordic & East' AND\n (epoch <= 161309 AND epoch > (161309 - 5))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT 100", + "query": "\n SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_happened = 1 AND att_inc_delay > 1 AND val_nos_name = '${nos_name_var}' AND epoch = ${epoch_number_var}\n ORDER BY val_id\n LIMIT 1 by val_id\n LIMIT ${limit}", + "rawQuery": "SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_happened = 1 AND att_inc_delay > 1 AND val_nos_name = 'Allnodes' AND epoch = 170501\n ORDER BY val_id\n LIMIT 1 by val_id\n LIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of attestations with high INC. DELAY (${epochs_range} epochs range)", + "title": "Validators with high INC. DELAY attestation", "type": "table" }, { @@ -3315,7 +3321,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3334,8 +3341,8 @@ "calcs": [ "lastNotNull" ], - "displayMode": "hidden", - "placement": "right", + "displayMode": "list", + "placement": "bottom", "sortBy": "Last *", "sortDesc": true }, @@ -3418,7 +3425,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3504,10 +3512,10 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { - "datasource": { + "datasource": { "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, @@ -3516,14 +3524,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\n SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_head = 0 AND val_nos_name = '${nos_name_var}' AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT ${limit}", - "rawQuery": "SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_head = 0 AND val_nos_name = 'EU Nordic & East' AND\n (epoch <= 161309 AND epoch > (161309 - 5))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT 100", + "query": "\n SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_head = 0 AND val_nos_name = '${nos_name_var}' AND epoch = ${epoch_number_var}\n ORDER BY val_id\n LIMIT 1 BY val_id\n LIMIT ${limit}", + "rawQuery": "SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_head = 0 AND val_nos_name = 'Allnodes' AND epoch = 170501\n ORDER BY val_id\n LIMIT 1 BY val_id\n LIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of attestations with invalid HEAD (${epochs_range} epochs range)", + "title": "Validators with invalid HEAD", "type": "table" }, { @@ -3571,7 +3579,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3590,8 +3599,8 @@ "calcs": [ "lastNotNull" ], - "displayMode": "hidden", - "placement": "right", + "displayMode": "list", + "placement": "bottom", "sortBy": "Last *", "sortDesc": true }, @@ -3674,7 +3683,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3748,10 +3758,10 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { - "datasource": { + "datasource": { "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, @@ -3760,14 +3770,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\n SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_target = 0 AND val_nos_name = '${nos_name_var}' AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT ${limit}", - "rawQuery": "SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_target = 0 AND val_nos_name = 'EU Nordic & East' AND\n (epoch <= 161309 AND epoch > (161309 - 5))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT 100", + "query": "\n SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_target = 0 AND val_nos_name = '${nos_name_var}' AND epoch = ${epoch_number_var}\n ORDER BY val_id\n LIMIT 1 by val_id\n LIMIT ${limit}", + "rawQuery": "SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_target = 0 AND val_nos_name = 'Allnodes' AND epoch = 170501\n ORDER BY val_id\n LIMIT 1 by val_id\n LIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of attestations with invalid TARGET (${epochs_range} epochs range)", + "title": "Validators with invalid TARGET", "type": "table" }, { @@ -3815,7 +3825,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3834,8 +3845,8 @@ "calcs": [ "lastNotNull" ], - "displayMode": "hidden", - "placement": "right", + "displayMode": "list", + "placement": "bottom", "sortBy": "Last *", "sortDesc": true }, @@ -3918,7 +3929,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -3992,10 +4004,10 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { - "datasource": { + "datasource": { "type": "vertamedia-clickhouse-datasource", "uid": "PDEE91DDB90597936" }, @@ -4004,14 +4016,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\n SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_source = 0 AND val_nos_name = '${nos_name_var}' AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT ${limit}", - "rawQuery": "SELECT\n count() as Fails,\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_source = 0 AND val_nos_name = 'EU Nordic & East' AND\n (epoch <= 161309 AND epoch > (161309 - 5))\n GROUP BY val_id\n ORDER BY Fails DESC, val_id\n LIMIT 100", + "query": "\n SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_source = 0 AND val_nos_name = '${nos_name_var}' AND epoch = ${epoch_number_var}\n ORDER BY val_id\n LIMIT 1 by val_id\n LIMIT ${limit}", + "rawQuery": "SELECT\n val_id as Validator\n FROM validators_summary\n WHERE att_valid_source = 0 AND val_nos_name = 'Allnodes' AND epoch = 170501\n ORDER BY val_id\n LIMIT 1 by val_id\n LIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of attestations with invalid SOURCE (${epochs_range} epochs range)", + "title": "Validator with invalid SOURCE", "type": "table" }, { @@ -4059,7 +4071,8 @@ "mode": "absolute", "steps": [ { - "color": "red" + "color": "red", + "value": null } ] } @@ -4078,8 +4091,8 @@ "calcs": [ "lastNotNull" ], - "displayMode": "hidden", - "placement": "right", + "displayMode": "list", + "placement": "bottom", "sortBy": "Last *", "sortDesc": true }, @@ -4119,8 +4132,8 @@ { "current": { "selected": false, - "text": "EU Nordic & East", - "value": "EU Nordic & East" + "text": "Allnodes", + "value": "Allnodes" }, "datasource": { "type": "prometheus", @@ -4147,8 +4160,8 @@ { "current": { "selected": false, - "text": "None", - "value": "" + "text": "170501", + "value": "170501" }, "datasource": { "type": "prometheus", @@ -4175,8 +4188,8 @@ { "current": { "selected": false, - "text": "None", - "value": "" + "text": "0", + "value": "0" }, "datasource": { "type": "prometheus", @@ -4275,8 +4288,8 @@ { "current": { "selected": false, - "text": "None", - "value": "" + "text": "95.4833984375", + "value": "95.4833984375" }, "datasource": { "type": "prometheus", @@ -4303,8 +4316,8 @@ { "current": { "selected": false, - "text": "None", - "value": "" + "text": "10", + "value": "10" }, "datasource": { "type": "prometheus", @@ -4340,6 +4353,6 @@ "timezone": "", "title": "NodeOperators", "uid": "3wimU2H7h", - "version": 10, + "version": 3, "weekStart": "" } diff --git a/docker/grafana/provisioning/dashboards/rewards.json b/docker/grafana/provisioning/dashboards/rewards.json new file mode 100644 index 00000000..11fd76bc --- /dev/null +++ b/docker/grafana/provisioning/dashboards/rewards.json @@ -0,0 +1,3316 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "iteration": 1672299121451, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": false, + "title": "Dashboard", + "tooltip": "", + "type": "dashboards", + "url": "" + } + ], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 60, + "panels": [], + "title": "App", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + }, + { + "color": "green", + "value": 0 + }, + { + "color": "dark-yellow", + "value": 1800000 + }, + { + "color": "red", + "value": 3600000 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 0, + "y": 1 + }, + "id": 1703, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "sum(ethereum_validators_monitoring_data_actuality)", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Data actuality", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Last processed for a specified time period", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "text", + "mode": "fixed" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-blue", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 5, + "y": 1 + }, + "id": 1760, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "sum(ethereum_validators_monitoring_epoch_number)", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Current epoch", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Related to 'inactivity leaks' or 'slashing case' or math accuracy", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-blue", + "value": null + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 20, + "y": 1 + }, + "id": 1762, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "(sum_over_time((sum(ethereum_validators_monitoring_operator_calculated_balance_calculation_error{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / sum_over_time((sum(ethereum_validators_monitoring_operator_real_balance_delta{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:])) * 100", + "refId": "A" + } + ], + "title": "Calculation error", + "transformations": [], + "type": "stat" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 1774, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 0, + "y": 6 + }, + "id": 1765, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "hidden", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ethereum_validators_monitoring_avg_chain_reward{duty=\"attestation\"} / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "avg", + "range": true, + "refId": "A" + } + ], + "title": "Attestation reward", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 8, + "y": 6 + }, + "id": 1766, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "hidden", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ethereum_validators_monitoring_avg_chain_reward{duty=\"proposal\"} / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "avg", + "range": true, + "refId": "A" + } + ], + "title": "Proposal reward", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 16, + "y": 6 + }, + "id": 1767, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "hidden", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ethereum_validators_monitoring_avg_chain_reward{duty=\"sync\"} / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "avg", + "range": true, + "refId": "A" + } + ], + "title": "Sync reward", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "fixed" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 0, + "y": 10 + }, + "id": 1768, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "hidden", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ethereum_validators_monitoring_avg_chain_penalty{duty=\"attestation\"} / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "avg", + "range": true, + "refId": "A" + } + ], + "title": "Attestation penalty", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "fixed" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 10 + }, + "id": 1769, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "hidden", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ethereum_validators_monitoring_avg_chain_penalty{duty=\"sync\"} / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "avg", + "range": true, + "refId": "A" + } + ], + "title": "Sync penalty", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "purple", + "mode": "fixed" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 0, + "y": 14 + }, + "id": 1770, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "hidden", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ethereum_validators_monitoring_avg_chain_missed_reward{duty=\"attestation\"} / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "avg", + "range": true, + "refId": "A" + } + ], + "title": "Attestation missed reward", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "purple", + "mode": "fixed" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 8, + "y": 14 + }, + "id": 1771, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "hidden", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ethereum_validators_monitoring_avg_chain_missed_reward{duty=\"proposal\"} / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "avg", + "range": true, + "refId": "A" + } + ], + "title": "Proposal missed reward", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "purple", + "mode": "fixed" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 16, + "y": 14 + }, + "id": 1772, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "hidden", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ethereum_validators_monitoring_avg_chain_missed_reward{duty=\"sync\"} / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "avg", + "range": true, + "refId": "A" + } + ], + "title": "Sync missed reward", + "type": "timeseries" + } + ], + "title": "⛓️ Average chain validator stats", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 6 + }, + "id": 1729, + "panels": [], + "title": "🏦 Total", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisGridShow": false, + "axisLabel": "ETH", + "axisPlacement": "left", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Rewards" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Penalties" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Missed rewards" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "reward median" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "line" + }, + { + "id": "custom.spanNulls", + "value": true + }, + { + "id": "custom.fillOpacity", + "value": 10 + }, + { + "id": "custom.lineWidth", + "value": 1 + }, + { + "id": "color", + "value": { + "fixedColor": "super-light-green", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "penalty median" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "line" + }, + { + "id": "custom.spanNulls", + "value": true + }, + { + "id": "custom.fillOpacity", + "value": 10 + }, + { + "id": "custom.lineWidth", + "value": 1 + }, + { + "id": "color", + "value": { + "fixedColor": "super-light-red", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "missed rewards median" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "line" + }, + { + "id": "custom.spanNulls", + "value": true + }, + { + "id": "custom.fillOpacity", + "value": 10 + }, + { + "id": "custom.lineWidth", + "value": 1 + }, + { + "id": "color", + "value": { + "fixedColor": "super-light-purple", + "mode": "fixed" + } + }, + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 1764, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by () (ethereum_validators_monitoring_operator_reward{nos_name=~'${nos_name_array_var}'}) / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "Rewards", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum by () (ethereum_validators_monitoring_operator_penalty{nos_name=~'${nos_name_array_var}'}) / 10^9 AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "hide": false, + "legendFormat": "Penalties", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum by () (ethereum_validators_monitoring_operator_missed_reward{nos_name=~'${nos_name_array_var}'}) / 10^9 AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "hide": false, + "legendFormat": "Missed rewards", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum by () (ethereum_validators_monitoring_operator_reward{nos_name=~'${nos_name_array_var}'}) / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "hide": false, + "legendFormat": "Rewards (line)", + "range": true, + "refId": "reward median" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum by () (ethereum_validators_monitoring_operator_penalty{nos_name=~'${nos_name_array_var}'}) / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "hide": false, + "legendFormat": "Penalties (line)", + "range": true, + "refId": "penalty median" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum by () (ethereum_validators_monitoring_operator_missed_reward{nos_name=~'${nos_name_array_var}'}) / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "hide": false, + "legendFormat": "Missed rewards (line)", + "range": true, + "refId": "missed rewards median" + } + ], + "title": "Life line", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "total_rewards - total_penalties", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ETH ⛏️" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 10, + "x": 0, + "y": 13 + }, + "id": 1732, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "sum_over_time((sum by () (ethereum_validators_monitoring_operator_calculated_balance_delta{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "refId": "A" + } + ], + "title": "Total earned", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "color-text", + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Rewards" + }, + "properties": [ + { + "id": "custom.width", + "value": 125 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Penalty" + }, + "properties": [ + { + "id": "custom.width", + "value": 125 + }, + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Missed rewards" + }, + "properties": [ + { + "id": "custom.width", + "value": 130 + }, + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Operator" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Earned" + }, + "properties": [ + { + "id": "custom.width", + "value": 125 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Calc error" + }, + "properties": [ + { + "id": "custom.width", + "value": 125 + }, + { + "id": "color", + "value": { + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 13, + "w": 14, + "x": 10, + "y": 13 + }, + "id": 1758, + "options": { + "footer": { + "enablePagination": true, + "fields": [], + "reducer": [ + "sum" + ], + "show": true + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Earned" + } + ] + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_reward{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "reward" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_penalty{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "penalty" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_missed_reward{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "missed" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_reward{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9) - (sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_penalty{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{nos_name}}", + "range": false, + "refId": "earned" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_calculated_balance_calculation_error{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{nos_name}}", + "range": false, + "refId": "error" + } + ], + "title": "Total summary (${epochs_range} epochs range)", + "transformations": [ + { + "id": "groupBy", + "options": { + "fields": { + "Value #all": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #att": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #bad": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #earned": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #error": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #good": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #missed": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #penalty": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #prop": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #reward": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #sync": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #total": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "nos_name": { + "aggregations": [], + "operation": "groupby" + } + } + } + }, + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": { + "Value #all (sum)": 3, + "Value #bad (sum)": 1, + "Value #good (sum)": 2, + "nos_name": 0 + }, + "renameByName": { + "Value #all (sum)": "All", + "Value #att (sum)": "Attestation", + "Value #bad (sum)": "Bad", + "Value #earned (sum)": "Earned", + "Value #error (sum)": "Calc error", + "Value #good (sum)": "Good", + "Value #missed (sum)": "Missed rewards", + "Value #penalty (sum)": "Penalty", + "Value #prop (sum)": "Proposal", + "Value #reward (sum)": "Rewards", + "Value #sync (sum)": "Sync", + "Value #total (sum)": "Total", + "nos_name": "Operator" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ETH" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 0, + "y": 19 + }, + "id": 1727, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "sum_over_time((sum by () (ethereum_validators_monitoring_operator_reward{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "refId": "A" + } + ], + "title": "Total rewards", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ETH" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 5, + "y": 19 + }, + "id": 1730, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "sum_over_time((sum by () (ethereum_validators_monitoring_operator_penalty{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "refId": "A" + } + ], + "title": "Total penalties", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "light-purple", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ETH" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 10, + "x": 0, + "y": 23 + }, + "id": 1731, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "sum_over_time((sum by () (ethereum_validators_monitoring_operator_missed_reward{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "refId": "A" + } + ], + "title": "Total missed rewards", + "type": "stat" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 1735, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "color-text", + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Proposal" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Attestation" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "custom.width", + "value": 101 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sync" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Operator" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + } + ] + }, + "gridPos": { + "h": 13, + "w": 11, + "x": 0, + "y": 27 + }, + "id": 1754, + "options": { + "footer": { + "enablePagination": true, + "fields": [], + "reducer": [ + "sum" + ], + "show": true + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Total" + } + ] + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_reward{duty=\"attestation\", nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "att" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_reward{duty=\"proposal\", nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "prop" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_reward{duty=\"sync\", nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "sync" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_reward{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{nos_name}}", + "range": false, + "refId": "total" + } + ], + "title": "Rewards summary (${epochs_range} epochs range)", + "transformations": [ + { + "id": "groupBy", + "options": { + "fields": { + "Value #all": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #att": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #bad": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #good": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #prop": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #sync": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #total": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "nos_name": { + "aggregations": [], + "operation": "groupby" + } + } + } + }, + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": { + "Value #all (sum)": 3, + "Value #bad (sum)": 1, + "Value #good (sum)": 2, + "nos_name": 0 + }, + "renameByName": { + "Value #all (sum)": "All", + "Value #att (sum)": "Attestation", + "Value #bad (sum)": "Bad", + "Value #good (sum)": "Good", + "Value #prop (sum)": "Proposal", + "Value #sync (sum)": "Sync", + "Value #total (sum)": "Total", + "nos_name": "Operator" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 13, + "x": 11, + "y": 27 + }, + "id": 1744, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "sum by (nos_name) (ethereum_validators_monitoring_operator_reward{nos_name=~'${nos_name_array_var}'}) / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "A" + } + ], + "title": "Per operator", + "type": "timeseries" + } + ], + "title": "🟩 Rewards", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 1737, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "color-text", + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-red" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Attestation" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sync" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Operator" + }, + "properties": [ + { + "id": "custom.filterable", + "value": false + }, + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 13, + "w": 11, + "x": 0, + "y": 41 + }, + "id": 1755, + "options": { + "footer": { + "enablePagination": true, + "fields": [], + "reducer": [ + "sum" + ], + "show": true + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Total" + } + ] + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_penalty{duty=\"attestation\", nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "att" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_penalty{duty=\"sync\", nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "sync" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_penalty{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{nos_name}}", + "range": false, + "refId": "total" + } + ], + "title": "Penalties summary (${epochs_range} epochs range)", + "transformations": [ + { + "id": "groupBy", + "options": { + "fields": { + "Value #all": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #att": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #bad": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #good": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #prop": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #sync": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #total": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "nos_name": { + "aggregations": [], + "operation": "groupby" + } + } + } + }, + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": { + "Value #all (sum)": 3, + "Value #bad (sum)": 1, + "Value #good (sum)": 2, + "nos_name": 0 + }, + "renameByName": { + "Value #all (sum)": "All", + "Value #att (sum)": "Attestation", + "Value #bad (sum)": "Bad", + "Value #good (sum)": "Good", + "Value #prop (sum)": "Proposal", + "Value #sync (sum)": "Sync", + "Value #total (sum)": "Total", + "nos_name": "Operator" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 13, + "x": 11, + "y": 41 + }, + "id": 1750, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "sum by (nos_name) (ethereum_validators_monitoring_operator_penalty{nos_name=~'${nos_name_array_var}'}) / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "A" + } + ], + "title": "Per operator", + "type": "timeseries" + } + ], + "title": "🟥 Penalties", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 1739, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "color-text", + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-purple" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Attestation" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sync" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Operator" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Proposal" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + } + ] + }, + "gridPos": { + "h": 13, + "w": 11, + "x": 0, + "y": 55 + }, + "id": 1756, + "options": { + "footer": { + "enablePagination": true, + "fields": [], + "reducer": [ + "sum" + ], + "show": true + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Total" + } + ] + }, + "pluginVersion": "8.5.15", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_missed_reward{duty=\"attestation\", nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "att" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_missed_reward{duty=\"proposal\", nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "prop" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_missed_reward{duty=\"sync\", nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "sync" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum_over_time((sum by (nos_name) (ethereum_validators_monitoring_operator_missed_reward{nos_name=~'${nos_name_array_var}'}) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0)[$__range:]) / 10^9", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{nos_name}}", + "range": false, + "refId": "total" + } + ], + "title": "Missed rewards summary (${epochs_range} epochs range)", + "transformations": [ + { + "id": "groupBy", + "options": { + "fields": { + "Value #A": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #all": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #att": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #bad": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #good": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #prop": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #sync": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "Value #total": { + "aggregations": [ + "sum" + ], + "operation": "aggregate" + }, + "nos_name": { + "aggregations": [], + "operation": "groupby" + } + } + } + }, + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": { + "Value #att (sum)": 1, + "Value #prop (sum)": 3, + "Value #sync (sum)": 2, + "Value #total (sum)": 4, + "nos_name": 0 + }, + "renameByName": { + "Value #A (sum)": "Proposal", + "Value #all (sum)": "All", + "Value #att (sum)": "Attestation", + "Value #bad (sum)": "Bad", + "Value #good (sum)": "Good", + "Value #prop (sum)": "Proposal", + "Value #sync (sum)": "Sync", + "Value #total (sum)": "Total", + "nos_name": "Operator" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 3, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 13, + "x": 11, + "y": 55 + }, + "id": 1751, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "sortBy": "Last *", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "sum by (nos_name) (ethereum_validators_monitoring_operator_missed_reward{nos_name=~'${nos_name_array_var}'}) / 10^9 and on() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", + "interval": "", + "legendFormat": "{{nos_name}}", + "refId": "A" + } + ], + "title": "Per operator", + "type": "timeseries" + } + ], + "title": "🟪 Missed rewards", + "type": "row" + } + ], + "refresh": "1m", + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "170506", + "value": "170506" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "query_result(ethereum_validators_monitoring_epoch_number)", + "description": "Last epoch number", + "hide": 2, + "includeAll": false, + "label": "Epoch number", + "multi": false, + "name": "epoch_number_var", + "options": [], + "query": { + "query": "query_result(ethereum_validators_monitoring_epoch_number)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "/.* ([^\\ ]*) .*/", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "0", + "value": "0" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "query_result(ethereum_validators_monitoring_sync_participation_distance_down_from_chain_avg)", + "description": "Sync participation distance down from Blockchain average", + "hide": 2, + "includeAll": false, + "label": "Sync participation distance down from chain avg", + "multi": false, + "name": "sync_participation_distance_var", + "options": [], + "query": { + "query": "query_result(ethereum_validators_monitoring_sync_participation_distance_down_from_chain_avg)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "/.* ([^\\ ]*) .*/", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "3", + "value": "3" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "label_values(ethereum_validators_monitoring_validator_count_miss_attestation_last_n_epoch, epoch_interval)", + "hide": 2, + "includeAll": false, + "multi": false, + "name": "att_epochs_var", + "options": [], + "query": { + "query": "label_values(ethereum_validators_monitoring_validator_count_miss_attestation_last_n_epoch, epoch_interval)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "3", + "value": "3" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "label_values(ethereum_validators_monitoring_validator_count_with_sync_participation_less_avg_last_n_epoch, epoch_interval)", + "hide": 2, + "includeAll": false, + "multi": false, + "name": "sync_epochs_var", + "options": [], + "query": { + "query": "label_values(ethereum_validators_monitoring_validator_count_with_sync_participation_less_avg_last_n_epoch, epoch_interval)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "98.35205078125", + "value": "98.35205078125" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "query_result(ethereum_validators_monitoring_chain_sync_participation_avg_percent)", + "description": "Chain sync participation average value", + "hide": 2, + "includeAll": false, + "label": "Chain sync participation avg", + "multi": false, + "name": "chain_sync_avg_participation", + "options": [], + "query": { + "query": "query_result(ethereum_validators_monitoring_chain_sync_participation_avg_percent)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "/.* ([^\\ ]*) .*/", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "9", + "value": "9" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "query_result(sum_over_time((changes(sum(ethereum_validators_monitoring_epoch_number)[25s:]) > 0)[$__range:]))", + "description": "Epochs range by prometheus metric", + "hide": 2, + "includeAll": false, + "label": "Epochs range", + "multi": false, + "name": "epochs_range", + "options": [], + "query": { + "query": "query_result(sum_over_time((changes(sum(ethereum_validators_monitoring_epoch_number)[25s:]) > 0)[$__range:]))", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "/.* ([^\\ ]*) .*/", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "query_result(ethereum_validators_monitoring_user_validators)", + "hide": 0, + "includeAll": true, + "label": "Node Operator", + "multi": true, + "name": "nos_name_var", + "options": [], + "query": { + "query": "query_result(ethereum_validators_monitoring_user_validators)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "/nos_name=\"(?[^\"]+)/", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "Allnodes|Anyblock Analytics|Attestant (BVI) Limited|Blockdaemon|Blockscape|BridgeTower|Certus One|ChainLayer|ChainSafe|Chorus One|ConsenSys Codefi|CryptoManufaktur|DSRV|Everstake|Figment|HashQuark|InfStones|Kukis Global|Nethermind|P2P.ORG - P2P Validator|Prysmatic Labs|RockLogic GmbH|RockX|Sigma Prime|Simply Staking|SkillZ|Stakely|Stakin|Staking Facilities|stakefish", + "value": "Allnodes|Anyblock Analytics|Attestant (BVI) Limited|Blockdaemon|Blockscape|BridgeTower|Certus One|ChainLayer|ChainSafe|Chorus One|ConsenSys Codefi|CryptoManufaktur|DSRV|Everstake|Figment|HashQuark|InfStones|Kukis Global|Nethermind|P2P.ORG - P2P Validator|Prysmatic Labs|RockLogic GmbH|RockX|Sigma Prime|Simply Staking|SkillZ|Stakely|Stakin|Staking Facilities|stakefish" + }, + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "PDEE91DDB90597936" + }, + "definition": "SELECT arrayStringConcat(array(${nos_name_var}), '|')", + "description": "Selected", + "hide": 2, + "includeAll": false, + "label": "Node operators names", + "multi": false, + "name": "nos_name_array_var", + "options": [], + "query": "SELECT arrayStringConcat(array(${nos_name_var}), '|')", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Rewards & Penalties", + "uid": "poBeaLaRD", + "version": 3, + "weekStart": "" +} diff --git a/docker/grafana/provisioning/dashboards/validators.json b/docker/grafana/provisioning/dashboards/validators.json index 7578e8e9..c40d8358 100644 --- a/docker/grafana/provisioning/dashboards/validators.json +++ b/docker/grafana/provisioning/dashboards/validators.json @@ -24,7 +24,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "iteration": 1668860249916, + "iteration": 1672295458681, "links": [ { "asDropdown": false, @@ -117,7 +117,7 @@ }, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -183,7 +183,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -247,7 +247,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -311,7 +311,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -328,25 +328,6 @@ "title": "Buffered ether", "type": "stat" }, - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "gridPos": { - "h": 4, - "w": 4, - "x": 20, - "y": 1 - }, - "id": 94, - "options": { - "content": "\"alt \n Made by Lido", - "mode": "html" - }, - "pluginVersion": "8.5.13", - "type": "text" - }, { "datasource": { "type": "prometheus", @@ -394,7 +375,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -475,7 +456,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -539,7 +520,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -610,7 +591,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -730,7 +711,7 @@ "showHeader": true, "sortBy": [] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -1515,7 +1496,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -2144,7 +2125,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -2945,7 +2926,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -3616,7 +3597,7 @@ "showUnfilled": true, "text": {} }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -3850,7 +3831,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -3862,8 +3843,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_nos_name as Operator,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id, val_nos_name \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = ${epoch_number_var}\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id, val_nos_name \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = ${epoch_number_var} - 6\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nORDER BY GWei ASC\nLIMIT 100", - "rawQuery": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_nos_name as Operator,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id, val_nos_name \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = 161306\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id, val_nos_name \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = 161306 - 6\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nORDER BY GWei ASC\nLIMIT 100", + "query": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_nos_name as Operator,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id, val_nos_name \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = ${epoch_number_var}\n LIMIT 1 by val_id\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id, val_nos_name \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = ${epoch_number_var} - 6\n LIMIT 1 by val_id\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nORDER BY GWei ASC\nLIMIT 100", + "rawQuery": "SELECT\n current.val_balance - previous.val_balance AS GWei,\n current.val_nos_name as Operator,\n current.val_id as Validator\nFROM \n (\n SELECT val_balance, val_id, val_nos_id, val_nos_name \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = 170495\n LIMIT 1 by val_id\n ) AS current\nLEFT JOIN\n (\n SELECT val_balance, val_id, val_nos_id, val_nos_name \n FROM validators_summary \n WHERE \n val_status != 'pending_queued' AND \n val_nos_id IS NOT NULL AND\n epoch = 170495 - 6\n LIMIT 1 by val_id\n ) AS previous\nON\n previous.val_nos_id = current.val_nos_id AND \n previous.val_id = current.val_id\nORDER BY GWei ASC\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true @@ -4110,7 +4091,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -4122,8 +4103,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n block_to_propose as Block,\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND val_nos_id IS NOT NULL AND block_proposed = 0 AND (epoch <= ${epoch_number_var} AND epoch >= (${epoch_number_var} - ${epochs_range}))\nORDER BY block_to_propose DESC, val_id", - "rawQuery": "SELECT\n block_to_propose as Block,\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND val_nos_id IS NOT NULL AND block_proposed = 0 AND (epoch <= 161306 AND epoch >= (161306 - 20))\nORDER BY block_to_propose DESC, val_id", + "query": "SELECT\n block_to_propose as Block,\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND val_nos_id IS NOT NULL AND block_proposed = 0 AND (epoch <= ${epoch_number_var} AND epoch >= (${epoch_number_var} - ${epochs_range}))\nORDER BY block_to_propose DESC, val_id\nLIMIT 1 by epoch, val_id", + "rawQuery": "SELECT\n block_to_propose as Block,\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE is_proposer = 1 AND val_nos_id IS NOT NULL AND block_proposed = 0 AND (epoch <= 170497 AND epoch >= (170497 - 10))\nORDER BY block_to_propose DESC, val_id\nLIMIT 1 by epoch, val_id", "refId": "A", "round": "0s", "skip_comments": true @@ -4312,7 +4293,7 @@ { "matcher": { "id": "byName", - "options": "user" + "options": "User" }, "properties": [ { @@ -4335,7 +4316,7 @@ { "matcher": { "id": "byName", - "options": "other" + "options": "Other" }, "properties": [ { @@ -4385,7 +4366,7 @@ "exemplar": true, "expr": "sum(ethereum_validators_monitoring_user_sync_participation_avg_percent) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", "interval": "", - "legendFormat": "user", + "legendFormat": "User", "refId": "A" }, { @@ -4397,7 +4378,7 @@ "expr": "sum(ethereum_validators_monitoring_other_sync_participation_avg_percent) AND ON() changes(ethereum_validators_monitoring_epoch_number[25s]) > 0", "hide": false, "interval": "", - "legendFormat": "other", + "legendFormat": "Other", "refId": "B" } ], @@ -4551,7 +4532,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -4563,8 +4544,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n sync_percent as Percent,\n val_nos_name as Operator,\n val_id as Validator\nFROM\n validators_summary\nWHERE is_sync = 1 AND val_nos_id IS NOT NULL AND epoch = ${epoch_number_var} AND sync_percent < (${chain_sync_avg_participation} - ${sync_participation_distance_var})\nORDER BY sync_percent", - "rawQuery": "SELECT\n sync_percent as Percent,\n val_nos_name as Operator,\n val_id as Validator\nFROM\n validators_summary\nWHERE is_sync = 1 AND val_nos_id IS NOT NULL AND epoch = 161306 AND sync_percent < (97.59954616427422 - 10)\nORDER BY sync_percent", + "query": "SELECT\n sync_percent as Percent,\n val_nos_name as Operator,\n val_id as Validator\nFROM\n validators_summary\nWHERE is_sync = 1 AND val_nos_id IS NOT NULL AND epoch = ${epoch_number_var} AND sync_percent < (${chain_sync_avg_participation} - ${sync_participation_distance_var})\nORDER BY sync_percent\nLIMIT 1 by epoch, val_id", + "rawQuery": "SELECT\n sync_percent as Percent,\n val_nos_name as Operator,\n val_id as Validator\nFROM\n validators_summary\nWHERE is_sync = 1 AND val_nos_id IS NOT NULL AND epoch = 170497 AND sync_percent < (97.8271484375 - 0)\nORDER BY sync_percent\nLIMIT 1 by epoch, val_id", "refId": "A", "round": "0s", "skip_comments": true @@ -4830,7 +4811,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -4842,14 +4823,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\nSELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_happened = 0 AND val_nos_id IS NOT NULL AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", - "rawQuery": "SELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_happened = 0 AND val_nos_id IS NOT NULL AND\n (epoch <= 161306 AND epoch > (161306 - 20))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", + "query": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_happened = 0 AND val_nos_id IS NOT NULL AND epoch = ${epoch_number_var}\nORDER BY val_id\nLIMIT 1 by val_id\nLIMIT 100", + "rawQuery": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_happened = 0 AND val_nos_id IS NOT NULL AND epoch = 170497\nORDER BY val_id\nLIMIT 1 by val_id\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of MISSED attestations (${epochs_range} epochs range)", + "title": "Validators with MISSED attestation", "type": "table" }, { @@ -5092,7 +5073,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -5104,14 +5085,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\nSELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_happened = 1 AND att_inc_delay > 1 AND val_nos_id IS NOT NULL AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", - "rawQuery": "SELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_happened = 1 AND att_inc_delay > 1 AND val_nos_id IS NOT NULL AND\n (epoch <= 161306 AND epoch > (161306 - 20))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", + "query": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_happened = 1 AND att_inc_delay > 1 AND val_nos_id IS NOT NULL AND epoch = ${epoch_number_var}\nORDER BY val_id\nLIMIT 1 by val_id\nLIMIT 100", + "rawQuery": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_happened = 1 AND att_inc_delay > 1 AND val_nos_id IS NOT NULL AND epoch = 170497\nORDER BY val_id\nLIMIT 1 by val_id\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of attestations with high INC. DELAY (${epochs_range} epochs range)", + "title": "Validators with high INC. DELAY attestation", "type": "table" }, { @@ -5353,7 +5334,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -5365,14 +5346,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\nSELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_valid_head = 0 AND val_nos_id IS NOT NULL AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", - "rawQuery": "SELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_valid_head = 0 AND val_nos_id IS NOT NULL AND\n (epoch <= 161306 AND epoch > (161306 - 20))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", + "query": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_valid_head = 0 AND val_nos_id IS NOT NULL AND epoch = ${epoch_number_var}\nORDER BY val_id\nLIMIT 1 by val_id\nLIMIT 100", + "rawQuery": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_valid_head = 0 AND val_nos_id IS NOT NULL AND epoch = 170497\nORDER BY val_id\nLIMIT 1 by val_id\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of attestations with invalid HEAD (${epochs_range} epochs range)", + "title": "Validators with invalid HEAD", "type": "table" }, { @@ -5614,7 +5595,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -5626,14 +5607,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\nSELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_valid_target = 0 AND val_nos_id IS NOT NULL AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", - "rawQuery": "SELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_valid_target = 0 AND val_nos_id IS NOT NULL AND\n (epoch <= 161306 AND epoch > (161306 - 20))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", + "query": "\nSELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_valid_target = 0 AND val_nos_id IS NOT NULL AND epoch = ${epoch_number_var}\nORDER BY val_id\nLIMIT 100", + "rawQuery": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_valid_target = 0 AND val_nos_id IS NOT NULL AND epoch = 170497\nORDER BY val_id\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of attestations with invalid TARGET (${epochs_range} epochs range)", + "title": "Validators with invalid TARGET", "type": "table" }, { @@ -5875,7 +5856,7 @@ } ] }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -5887,14 +5868,14 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "\nSELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_valid_source = 0 AND val_nos_id IS NOT NULL AND\n (epoch <= ${epoch_number_var} AND epoch > (${epoch_number_var} - ${epochs_range}))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", - "rawQuery": "SELECT\n count() as Fails,\n val_nos_name as Operator,\n val_id as Validator\nFROM (\n SELECT\n val_id,\n val_nos_name\n FROM validators_summary\n WHERE att_valid_source = 0 AND val_nos_id IS NOT NULL AND\n (epoch <= 161306 AND epoch > (161306 - 20))\n ORDER BY val_id\n)\nGROUP BY val_id, val_nos_name\nORDER BY Fails DESC, val_id\nLIMIT 100", + "query": "\nSELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_valid_source = 0 AND val_nos_id IS NOT NULL AND epoch = ${epoch_number_var}\nORDER BY val_id\nLIMIT 1 by val_id\nLIMIT 100", + "rawQuery": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE att_valid_source = 0 AND val_nos_id IS NOT NULL AND epoch = 170497\nORDER BY val_id\nLIMIT 1 by val_id\nLIMIT 100", "refId": "A", "round": "0s", "skip_comments": true } ], - "title": "Number of attestations with invalid SOURCE (${epochs_range} epochs range)", + "title": "Validator with invalid SOURCE", "type": "table" }, { @@ -6095,7 +6076,7 @@ }, "showHeader": true }, - "pluginVersion": "8.5.13", + "pluginVersion": "8.5.15", "targets": [ { "datasource": { @@ -6107,8 +6088,8 @@ "format": "table", "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", "intervalFactor": 1, - "query": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE (val_status == 'active_slashed' OR val_status == 'exited_slashed' OR val_slashed == 1) AND val_nos_id IS NOT NULL and epoch = ${epoch_number_var}", - "rawQuery": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE (val_status == 'active_slashed' OR val_status == 'exited_slashed' OR val_slashed == 1) AND val_nos_id IS NOT NULL and epoch = 161306", + "query": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE (val_status == 'active_slashed' OR val_status == 'exited_slashed' OR val_slashed == 1) AND val_nos_id IS NOT NULL and epoch = ${epoch_number_var}\nLIMIT 1 by val_id", + "rawQuery": "SELECT\n val_nos_name as Operator,\n val_id as Validator\nFROM validators_summary\nWHERE (val_status == 'active_slashed' OR val_status == 'exited_slashed' OR val_slashed == 1) AND val_nos_id IS NOT NULL and epoch = 170497\nLIMIT 1 by val_id", "refId": "A", "round": "0s", "skip_comments": true @@ -6346,8 +6327,8 @@ { "current": { "selected": false, - "text": "161306", - "value": "161306" + "text": "170497", + "value": "170497" }, "datasource": { "type": "prometheus", @@ -6374,8 +6355,8 @@ { "current": { "selected": false, - "text": "10", - "value": "10" + "text": "0", + "value": "0" }, "datasource": { "type": "prometheus", @@ -6402,8 +6383,8 @@ { "current": { "selected": false, - "text": "97.59954616427422", - "value": "97.59954616427422" + "text": "97.8271484375", + "value": "97.8271484375" }, "datasource": { "type": "prometheus", @@ -6430,8 +6411,8 @@ { "current": { "selected": false, - "text": "20", - "value": "20" + "text": "9", + "value": "9" }, "datasource": { "type": "prometheus", @@ -6474,6 +6455,6 @@ "timezone": "", "title": "Validators", "uid": "HRgPmpNnz", - "version": 11, + "version": 3, "weekStart": "" } diff --git a/package.json b/package.json index 08d89535..3f004b20 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "license": "MIT", "description": "Consensus layer validators monitoring bot, that fetches Lido or User Node Operator keys from Execution layer and checks their performance in Consensus layer by: balance delta, attestations, proposes, sync committee participation.", "dependencies": { - "@chainsafe/ssz": "^0.9.2", - "@discoveryjs/json-ext": "^0.5.7", + "@chainsafe/ssz": "^0.9.4", + "@clickhouse/client": "^0.0.11", "@ethersproject/abstract-signer": "^5.4.0", "@ethersproject/address": "^5.4.0", "@ethersproject/bignumber": "^5.4.1", @@ -49,7 +49,6 @@ "app-root-path": "^3.0.0", "class-transformer": "^0.4.0", "class-validator": "^0.13.1", - "clickhouse": "^2.6.0", "dotenv": "^10.0.0", "ethers": "^5.4.2", "fp-ts": "^2.11.5", @@ -61,6 +60,8 @@ "prom-client": "^13.2.0", "reflect-metadata": "^0.1.13", "retry-ts": "^0.1.3", + "stream-chain": "^2.2.5", + "stream-json": "^1.7.5", "typechain": "^5.2.0" }, "devDependencies": { @@ -118,4 +119,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} diff --git a/src/common/alertmanager/alerts/BasicAlert.ts b/src/common/alertmanager/alerts/BasicAlert.ts index 1c290bc3..bf34d1b1 100644 --- a/src/common/alertmanager/alerts/BasicAlert.ts +++ b/src/common/alertmanager/alerts/BasicAlert.ts @@ -1,4 +1,5 @@ import { ConfigService } from 'common/config'; +import { RegistrySourceOperator } from 'common/validators-registry'; import { ClickhouseService } from 'storage'; export interface AlertRequestBody { @@ -23,11 +24,13 @@ export abstract class Alert { protected sendTimestamp = 0; protected readonly config: ConfigService; protected readonly storage: ClickhouseService; + protected readonly operators: RegistrySourceOperator[]; - protected constructor(name: string, config: ConfigService, storage: ClickhouseService) { + protected constructor(name: string, config: ConfigService, storage: ClickhouseService, operators: RegistrySourceOperator[]) { this.alertname = name; this.config = config; this.storage = storage; + this.operators = operators; } abstract alertRule(bySlot: bigint): Promise; diff --git a/src/common/alertmanager/alerts/CriticalMissedAttestations.ts b/src/common/alertmanager/alerts/CriticalMissedAttestations.ts index b0b18263..04200c40 100644 --- a/src/common/alertmanager/alerts/CriticalMissedAttestations.ts +++ b/src/common/alertmanager/alerts/CriticalMissedAttestations.ts @@ -2,6 +2,7 @@ import { join } from 'lodash'; import { sentAlerts } from 'common/alertmanager'; import { ConfigService } from 'common/config'; +import { RegistrySourceOperator } from 'common/validators-registry'; import { ClickhouseService } from 'storage'; import { Alert, AlertRequestBody, AlertRuleResult } from './BasicAlert'; @@ -9,19 +10,20 @@ import { Alert, AlertRequestBody, AlertRuleResult } from './BasicAlert'; const VALIDATORS_WITH_MISSED_ATTESTATION_COUNT_THRESHOLD = 1 / 3; export class CriticalMissedAttestations extends Alert { - constructor(config: ConfigService, storage: ClickhouseService) { - super(CriticalMissedAttestations.name, config, storage); + constructor(config: ConfigService, storage: ClickhouseService, operators: RegistrySourceOperator[]) { + super(CriticalMissedAttestations.name, config, storage, operators); } async alertRule(epoch: bigint): Promise { const result: AlertRuleResult = {}; - const operators = await this.storage.getUserNodeOperatorsStats(epoch); + const nosStats = await this.storage.getUserNodeOperatorsStats(epoch); const missedAttValidatorsCount = await this.storage.getValidatorCountWithMissedAttestationsLastNEpoch(epoch); - for (const operator of operators.filter((o) => o.active_ongoing > this.config.get('CRITICAL_ALERTS_MIN_VAL_COUNT'))) { - const missedAtt = missedAttValidatorsCount.find((a) => a.val_nos_name == operator.val_nos_name); + for (const noStats of nosStats.filter((o) => o.active_ongoing > this.config.get('CRITICAL_ALERTS_MIN_VAL_COUNT'))) { + const operator = this.operators.find((o) => +noStats.val_nos_id == o.index); + const missedAtt = missedAttValidatorsCount.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); if (!missedAtt) continue; - if (missedAtt.amount > operator.active_ongoing * VALIDATORS_WITH_MISSED_ATTESTATION_COUNT_THRESHOLD) { - result[operator.val_nos_name] = { ongoing: operator.active_ongoing, missedAtt: missedAtt.amount }; + if (missedAtt.amount > noStats.active_ongoing * VALIDATORS_WITH_MISSED_ATTESTATION_COUNT_THRESHOLD) { + result[operator.name] = { ongoing: noStats.active_ongoing, missedAtt: missedAtt.amount }; } } return result; diff --git a/src/common/alertmanager/alerts/CriticalMissedProposes.ts b/src/common/alertmanager/alerts/CriticalMissedProposes.ts index 55c7d956..a2cd4faf 100644 --- a/src/common/alertmanager/alerts/CriticalMissedProposes.ts +++ b/src/common/alertmanager/alerts/CriticalMissedProposes.ts @@ -2,24 +2,28 @@ import { join } from 'lodash'; import { sentAlerts } from 'common/alertmanager'; import { ConfigService } from 'common/config'; +import { RegistrySourceOperator } from 'common/validators-registry'; import { ClickhouseService } from 'storage'; import { Alert, AlertRequestBody, AlertRuleResult } from './BasicAlert'; +const VALIDATORS_WITH_MISSED_PROPOSALS_COUNT_THRESHOLD = 1 / 3; + export class CriticalMissedProposes extends Alert { - constructor(config: ConfigService, storage: ClickhouseService) { - super(CriticalMissedProposes.name, config, storage); + constructor(config: ConfigService, storage: ClickhouseService, operators: RegistrySourceOperator[]) { + super(CriticalMissedProposes.name, config, storage, operators); } async alertRule(epoch: bigint): Promise { const result: AlertRuleResult = {}; - const operators = await this.storage.getUserNodeOperatorsStats(epoch); + const nosStats = await this.storage.getUserNodeOperatorsStats(epoch); const proposes = await this.storage.getUserNodeOperatorsProposesStats(epoch); // ~12h range - for (const operator of operators.filter((o) => o.active_ongoing > this.config.get('CRITICAL_ALERTS_MIN_VAL_COUNT'))) { - const proposeStats = proposes.find((a) => a.val_nos_name == operator.val_nos_name); + for (const noStats of nosStats.filter((o) => o.active_ongoing > this.config.get('CRITICAL_ALERTS_MIN_VAL_COUNT'))) { + const operator = this.operators.find((o) => +noStats.val_nos_id == o.index); + const proposeStats = proposes.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); if (!proposeStats) continue; - if (proposeStats.missed > proposeStats.all / 3) { - result[operator.val_nos_name] = { all: proposeStats.all, missed: proposeStats.missed }; + if (proposeStats.missed > proposeStats.all * VALIDATORS_WITH_MISSED_PROPOSALS_COUNT_THRESHOLD) { + result[operator.name] = { all: proposeStats.all, missed: proposeStats.missed }; } } return result; diff --git a/src/common/alertmanager/alerts/CriticalNegativeDelta.ts b/src/common/alertmanager/alerts/CriticalNegativeDelta.ts index 2c694a4d..fb809c1f 100644 --- a/src/common/alertmanager/alerts/CriticalNegativeDelta.ts +++ b/src/common/alertmanager/alerts/CriticalNegativeDelta.ts @@ -2,24 +2,28 @@ import { join } from 'lodash'; import { sentAlerts } from 'common/alertmanager'; import { ConfigService } from 'common/config'; +import { RegistrySourceOperator } from 'common/validators-registry'; import { ClickhouseService } from 'storage'; import { Alert, AlertRequestBody, AlertRuleResult } from './BasicAlert'; +const VALIDATORS_WITH_NEGATIVE_DELTA_COUNT_THRESHOLD = 1 / 3; + export class CriticalNegativeDelta extends Alert { - constructor(config: ConfigService, storage: ClickhouseService) { - super(CriticalNegativeDelta.name, config, storage); + constructor(config: ConfigService, storage: ClickhouseService, operators: RegistrySourceOperator[]) { + super(CriticalNegativeDelta.name, config, storage, operators); } async alertRule(epoch: bigint): Promise { const result: AlertRuleResult = {}; - const operators = await this.storage.getUserNodeOperatorsStats(epoch); + const nosStats = await this.storage.getUserNodeOperatorsStats(epoch); const negativeValidatorsCount = await this.storage.getValidatorsCountWithNegativeDelta(epoch); - for (const operator of operators.filter((o) => o.active_ongoing > this.config.get('CRITICAL_ALERTS_MIN_VAL_COUNT'))) { - const negDelta = negativeValidatorsCount.find((a) => a.val_nos_name == operator.val_nos_name); + for (const noStats of nosStats.filter((o) => o.active_ongoing > this.config.get('CRITICAL_ALERTS_MIN_VAL_COUNT'))) { + const operator = this.operators.find((o) => +noStats.val_nos_id == o.index); + const negDelta = negativeValidatorsCount.find((a) => +a.val_nos_id == operator.index); if (!negDelta) continue; - if (negDelta.neg_count > operator.active_ongoing / 3) { - result[operator.val_nos_name] = { ongoing: operator.active_ongoing, negDelta: negDelta.neg_count }; + if (negDelta.neg_count > noStats.active_ongoing * VALIDATORS_WITH_NEGATIVE_DELTA_COUNT_THRESHOLD) { + result[operator.name] = { ongoing: noStats.active_ongoing, negDelta: negDelta.neg_count }; } } return result; diff --git a/src/common/alertmanager/alerts/CriticalSlashing.ts b/src/common/alertmanager/alerts/CriticalSlashing.ts index 23290110..ad5bc384 100644 --- a/src/common/alertmanager/alerts/CriticalSlashing.ts +++ b/src/common/alertmanager/alerts/CriticalSlashing.ts @@ -1,25 +1,27 @@ import { join } from 'lodash'; import { ConfigService } from 'common/config'; +import { RegistrySourceOperator } from 'common/validators-registry'; import { ClickhouseService } from 'storage'; import { Alert, AlertRequestBody, AlertRuleResult } from './BasicAlert'; export class CriticalSlashing extends Alert { - constructor(config: ConfigService, storage: ClickhouseService) { - super(CriticalSlashing.name, config, storage); + constructor(config: ConfigService, storage: ClickhouseService, operators: RegistrySourceOperator[]) { + super(CriticalSlashing.name, config, storage, operators); } async alertRule(epoch: bigint): Promise { const result: AlertRuleResult = {}; const currOperators = await this.storage.getUserNodeOperatorsStats(epoch); const prevOperators = await this.storage.getUserNodeOperatorsStats(epoch - BigInt(this.config.get('FETCH_INTERVAL_SLOTS'))); // compare with previous epoch - for (const currOperator of currOperators.filter((o) => o.active_ongoing > this.config.get('CRITICAL_ALERTS_MIN_VAL_COUNT'))) { - const prevOperator = prevOperators.find((a) => a.val_nos_name == currOperator.val_nos_name); + for (const currOperator of currOperators) { + const operator = this.operators.find((o) => +currOperator.val_nos_id == o.index); + const prevOperator = prevOperators.find((a) => a.val_nos_id == currOperator.val_nos_id); // if count of slashed validators increased, we should alert about it const prevSlashed = prevOperator ? prevOperator.slashed : 0; if (currOperator.slashed > prevSlashed) { - result[currOperator.val_nos_name] = { ongoing: currOperator.active_ongoing, slashed: currOperator.slashed - prevSlashed }; + result[operator.name] = { ongoing: currOperator.active_ongoing, slashed: currOperator.slashed - prevSlashed }; } } return result; diff --git a/src/common/alertmanager/critical-alerts.module.ts b/src/common/alertmanager/critical-alerts.module.ts index cbb957db..3e935aef 100644 --- a/src/common/alertmanager/critical-alerts.module.ts +++ b/src/common/alertmanager/critical-alerts.module.ts @@ -1,9 +1,12 @@ import { Module } from '@nestjs/common'; -import { CriticalAlertsService } from './critical-alerts.service'; + +import { RegistryModule } from 'common/validators-registry'; import { ClickhouseModule } from 'storage/clickhouse'; +import { CriticalAlertsService } from './critical-alerts.service'; + @Module({ - imports: [ClickhouseModule], + imports: [RegistryModule, ClickhouseModule], providers: [CriticalAlertsService], exports: [CriticalAlertsService], }) diff --git a/src/common/alertmanager/critical-alerts.service.ts b/src/common/alertmanager/critical-alerts.service.ts index ee493972..c09db040 100644 --- a/src/common/alertmanager/critical-alerts.service.ts +++ b/src/common/alertmanager/critical-alerts.service.ts @@ -4,6 +4,7 @@ import got from 'got'; import { ConfigService } from 'common/config'; import { PrometheusService } from 'common/prometheus'; +import { RegistryService, RegistrySourceOperator } from 'common/validators-registry'; import { ClickhouseService } from 'storage'; import { AlertRequestBody, PreparedToSendAlert } from './alerts/BasicAlert'; @@ -21,17 +22,20 @@ export const sentAlerts: SentAlerts = {}; @Injectable() export class CriticalAlertsService { private readonly baseUrl; + protected operators: RegistrySourceOperator[]; public constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, protected readonly config: ConfigService, protected readonly storage: ClickhouseService, protected readonly prometheus: PrometheusService, + protected readonly registryService: RegistryService, ) { this.baseUrl = this.config.get('CRITICAL_ALERTS_ALERTMANAGER_URL') ?? ''; } public async send(epoch: bigint) { + this.operators = await this.registryService.getOperators(); if (this.prometheus.getSlotTimeDiffWithNow() > 3600000) { this.logger.warn(`Data actuality greater than 1 hour. Critical alerts are suppressed`); return; @@ -57,10 +61,10 @@ export class CriticalAlertsService { private get alerts() { return [ - new CriticalNegativeDelta(this.config, this.storage), - new CriticalMissedProposes(this.config, this.storage), - new CriticalMissedAttestations(this.config, this.storage), - new CriticalSlashing(this.config, this.storage), + new CriticalNegativeDelta(this.config, this.storage, this.operators), + new CriticalMissedProposes(this.config, this.storage, this.operators), + new CriticalMissedAttestations(this.config, this.storage, this.operators), + new CriticalSlashing(this.config, this.storage, this.operators), ]; } diff --git a/src/common/config/env.validation.ts b/src/common/config/env.validation.ts index 2a502260..c4ecebf6 100644 --- a/src/common/config/env.validation.ts +++ b/src/common/config/env.validation.ts @@ -117,7 +117,7 @@ export class EnvironmentVariables { @IsNumber() @Min(100) @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) - public DB_INSERT_CHUNK_SIZE = 100_000; + public DB_INSERT_CHUNK_SIZE = 100000; @IsNotEmpty() @IsInt() @@ -146,17 +146,7 @@ export class EnvironmentVariables { @IsNumber() @Min(5000) @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) - public CL_API_GET_RESPONSE_TIMEOUT = 60000; - - @IsNumber() - @Min(10000) - @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) - public CL_API_POST_RESPONSE_TIMEOUT = 60000; - - @IsNumber() - @Min(10000) - @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) - public CL_API_POST_REQUEST_CHUNK_SIZE = 30000; + public CL_API_GET_RESPONSE_TIMEOUT = 15000; @IsNumber() @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) diff --git a/src/common/eth-providers/consensus-provider/consensus-provider.service.ts b/src/common/eth-providers/consensus-provider/consensus-provider.service.ts index c060188e..84d32ea7 100644 --- a/src/common/eth-providers/consensus-provider/consensus-provider.service.ts +++ b/src/common/eth-providers/consensus-provider/consensus-provider.service.ts @@ -1,6 +1,3 @@ -import { Readable } from 'stream'; - -import { parseChunked } from '@discoveryjs/json-ext'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; @@ -16,19 +13,15 @@ import { PrometheusService, TrackCLRequest } from 'common/prometheus'; import { BlockCache, BlockCacheService } from './block-cache'; import { MaxDeepError, ResponseError, errCommon, errRequest } from './errors'; import { - AttestationCommitteeInfo, - AttesterDutyInfo, BlockHeaderResponse, BlockInfoResponse, FinalityCheckpointsResponse, GenesisResponse, ProposerDutyInfo, - StateValidatorResponse, - SyncCommitteeDutyInfo, SyncCommitteeInfo, VersionResponse, } from './intefaces'; -import { BlockId, Epoch, RootHex, Slot, StateId, ValidatorIndex } from './types'; +import { BlockId, Epoch, RootHex, Slot, StateId } from './types'; interface RequestRetryOptions { maxRetries?: number; @@ -37,6 +30,29 @@ interface RequestRetryOptions { useFallbackOnResolved?: (r: any) => boolean; } +const REQUEST_TIMEOUT_POLICY_MS = { + // Starts when a socket is assigned. + // Ends when the hostname has been resolved. + lookup: undefined, + // Starts when lookup completes. + // Ends when the socket is fully connected. + // If lookup does not apply to the request, this event starts when the socket is assigned and ends when the socket is connected. + connect: 1000, + // Starts when connect completes. + // Ends when the handshake process completes. + secureConnect: undefined, + // Starts when the socket is connected. + // Resets when new data is transferred. + socket: undefined, + // Starts when the socket is connected. + // Ends when all data have been written to the socket. + send: undefined, + // Starts when request has been flushed. + // Ends when the headers are received. + // Will be redefined by `CL_API_GET_RESPONSE_TIMEOUT` + response: 1000, +}; + @Injectable() export class ConsensusProviderService { protected apiUrls: string[]; @@ -232,8 +248,10 @@ export class ConsensusProviderService { return blockInfo; } - public async getValidatorsState(stateId: StateId): Promise { - return await this.retryRequest((apiURL: string) => this.apiLargeGet(apiURL, this.endpoints.validatorsState(stateId))); + public async getValidatorsState(stateId: StateId): Promise { + return await this.retryRequest(async (apiURL: string) => await this.apiGetStream(apiURL, this.endpoints.validatorsState(stateId)), { + dataOnly: false, + }); } public async getBlockInfo(blockId: BlockId): Promise { @@ -260,68 +278,19 @@ export class ConsensusProviderService { return blockInfo; } - public async getAttestationCommitteesInfo(stateId: StateId, epoch: Epoch): Promise { - return await this.retryRequest((apiURL: string) => this.apiLargeGet(apiURL, this.endpoints.attestationCommittees(stateId, epoch))); + public async getAttestationCommitteesInfo(stateId: StateId, epoch: Epoch): Promise { + return await this.retryRequest( + async (apiURL: string) => await this.apiGetStream(apiURL, this.endpoints.attestationCommittees(stateId, epoch)), + { + dataOnly: false, + }, + ); } public async getSyncCommitteeInfo(stateId: StateId, epoch: Epoch): Promise { return await this.retryRequest((apiURL: string) => this.apiGet(apiURL, this.endpoints.syncCommittee(stateId, epoch))); } - public async getCanonicalAttesterDuties( - epoch: Epoch, - dependentRoot: RootHex, - indexes: ValidatorIndex[], - maxRetriesForGetCanonical = 3, - ): Promise { - const retry = retrier(this.logger, maxRetriesForGetCanonical, 100, 10000, true); - const request = async () => { - const res = <{ dependent_root: string; data: AttesterDutyInfo[] }>( - await this.retryRequest( - (apiURL: string) => this.apiLargePost(apiURL, this.endpoints.attesterDuties(epoch), { body: JSON.stringify(indexes) }), - { dataOnly: false }, - ) - ); - if (res.dependent_root != dependentRoot) { - throw Error(`Attester duty dependent root is not as expected. Actual: ${res.dependent_root} Expected: ${dependentRoot}`); - } - return res.data; - }; - - return await request() - .catch(() => retry(() => request())) - .catch(() => { - throw Error(`Failed to get canonical attester duty info after ${maxRetriesForGetCanonical} retries`); - }); - } - - public async getChunkedAttesterDuties(epoch: Epoch, dependentRoot: RootHex, indexes: string[]): Promise { - let result: AttesterDutyInfo[] = []; - const chunked = [...indexes]; - while (chunked.length > 0) { - const chunk = chunked.splice(0, this.config.get('CL_API_POST_REQUEST_CHUNK_SIZE')); // large payload may cause endpoint exception - result = result.concat(await this.getCanonicalAttesterDuties(epoch, dependentRoot, chunk)); - } - return result; - } - - public async getSyncCommitteeDuties(epoch: Epoch, indexes: string[]): Promise { - let result: SyncCommitteeDutyInfo[] = []; - const chunked = [...indexes]; - while (chunked.length > 0) { - const chunk = chunked.splice(0, this.config.get('CL_API_POST_REQUEST_CHUNK_SIZE')); // large payload may cause endpoint exception - result = result.concat( - await this.retryRequest((apiURL: string) => - this.apiLargePost(apiURL, this.endpoints.syncCommitteeDuties(epoch), { body: JSON.stringify(chunk) }), - ).catch((e) => { - this.logger.error('Unexpected status code while fetching sync committee duties info'); - throw e; - }), - ); - } - return result; - } - public async getCanonicalProposerDuties( epoch: Epoch, dependentRoot: RootHex, @@ -354,6 +323,8 @@ export class ConsensusProviderService { } protected async retryRequest(callback: (apiURL: string) => any, options?: RequestRetryOptions): Promise { + // todo: there are some problems with request timeout if host is unreachable. + // sometimes they cannot be caught, so they freeze requests and don't switch to fallback options = { maxRetries: options?.maxRetries ?? this.config.get('CL_API_MAX_RETRIES'), dataOnly: options?.dataOnly ?? true, @@ -398,7 +369,7 @@ export class ConsensusProviderService { @TrackCLRequest protected async apiGet(apiURL: string, subUrl: string): Promise { const res = await got - .get(urljoin(apiURL, subUrl), { timeout: { response: this.config.get('CL_API_GET_RESPONSE_TIMEOUT') } }) + .get(urljoin(apiURL, subUrl), { timeout: { ...REQUEST_TIMEOUT_POLICY_MS, response: this.config.get('CL_API_GET_RESPONSE_TIMEOUT') } }) .catch((e) => { if (e.response) { throw new ResponseError(errRequest(e.response.body, subUrl, apiURL), e.response.statusCode); @@ -416,32 +387,25 @@ export class ConsensusProviderService { } @TrackCLRequest - protected async apiLargeGet(apiURL: string, subUrl: string): Promise { - const readStream = got.stream.get(urljoin(apiURL, subUrl), { timeout: { response: this.config.get('CL_API_GET_RESPONSE_TIMEOUT') } }); - return await this._requestStream(readStream, subUrl, apiURL); - } - - @TrackCLRequest - protected async apiLargePost(apiURL: string, subUrl: string, params?: Record): Promise { - const readStream = got.stream.post(urljoin(apiURL, subUrl), { - timeout: { response: this.config.get('CL_API_POST_RESPONSE_TIMEOUT') }, - ...params, + protected async apiGetStream(apiURL: string, subUrl: string): Promise { + const readStream = got.stream.get(urljoin(apiURL, subUrl), { + timeout: { ...REQUEST_TIMEOUT_POLICY_MS, response: this.config.get('CL_API_GET_RESPONSE_TIMEOUT') }, }); - return await this._requestStream(readStream, subUrl, apiURL); - } - - protected async _requestStream(req: Readable, subUrl: string, apiURL: string) { - return await parseChunked( - req.on('response', (r: Response) => { - if (r.statusCode != 200) throw new HTTPError(r); - }), - ) - .catch((e) => { - if (e instanceof HTTPError) { - throw new ResponseError(errRequest(e.response.body, subUrl, apiURL), e.response.statusCode); - } - throw new ResponseError(errCommon(e.message, subUrl, apiURL)); + const promisedStream = async () => + new Promise((resolve, reject) => { + readStream.on('response', (r: Response) => { + if (r.statusCode != 200) reject(new HTTPError(r)); + resolve(readStream); + }); + readStream.on('error', (e) => reject(e)); }) - .finally(() => req.destroy()); + .then((r: Request) => r) + .catch((e) => { + if (e instanceof HTTPError) { + throw new ResponseError(errRequest(e.response.body, subUrl, apiURL), e.response.statusCode); + } + throw new ResponseError(errCommon(e.message, subUrl, apiURL)); + }); + return await promisedStream(); } } diff --git a/src/common/prometheus/prometheus.constants.ts b/src/common/prometheus/prometheus.constants.ts index 660a4da2..6db5054b 100644 --- a/src/common/prometheus/prometheus.constants.ts +++ b/src/common/prometheus/prometheus.constants.ts @@ -13,11 +13,15 @@ export const METRIC_TASK_DURATION_SECONDS = `task_duration_seconds`; export const METRIC_TASK_RESULT_COUNT = `task_result_count`; export const METRIC_DATA_ACTUALITY = `data_actuality`; +export const METRIC_USER_OPERATORS_IDENTIFIES = `user_operators_identifies`; export const METRIC_VALIDATORS = `validators`; export const METRIC_USER_VALIDATORS = `user_validators`; export const METRIC_FETCH_INTERVAL = `fetch_interval`; export const METRIC_SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG = `sync_participation_distance_down_from_chain_avg`; export const METRIC_VALIDATOR_BALANCES_DELTA = `validator_balances_delta`; +export const METRIC_OPERATOR_REAL_BALANCE_DELTA = `operator_real_balance_delta`; +export const METRIC_OPERATOR_CALCULATED_BALANCE_DELTA = `operator_calculated_balance_delta`; +export const METRIC_OPERATOR_CALCULATED_BALANCE_CALCULATION_ERROR = `operator_calculated_balance_calculation_error`; export const METRIC_VALIDATOR_QUANTILE_001_BALANCES_DELTA = `validator_quantile_001_balances_delta`; export const METRIC_VALIDATOR_COUNT_WITH_NEGATIVE_BALANCES_DELTA = `validator_count_with_negative_balances_delta`; export const METRIC_OTHER_VALIDATOR_COUNT_WITH_GOOD_SYNC_PARTICIPATION = `other_validator_count_with_good_sync_participation`; @@ -50,5 +54,11 @@ export const METRIC_CHAIN_SYNC_PARTICIPATION_AVG_PERCENT = `chain_sync_participa export const METRIC_EPOCH_NUMBER = `epoch_number`; export const METRIC_TOTAL_BALANCE_24H_DIFFERENCE = `total_balance_24h_difference`; export const METRIC_OPERATOR_BALANCE_24H_DIFFERENCE = `operator_balance_24h_difference`; +export const METRIC_AVG_CHAIN_REWARD = `avg_chain_reward`; +export const METRIC_OPERATOR_REWARD = `operator_reward`; +export const METRIC_AVG_CHAIN_MISSED_REWARD = `avg_chain_missed_reward`; +export const METRIC_OPERATOR_MISSED_REWARD = `operator_missed_reward`; +export const METRIC_AVG_CHAIN_PENALTY = `avg_chain_penalty`; +export const METRIC_OPERATOR_PENALTY = `operator_penalty`; export const METRIC_CONTRACT_KEYS_TOTAL = `contract_keys_total`; export const METRIC_STETH_BUFFERED_ETHER_TOTAL = `steth_buffered_ether_total`; diff --git a/src/common/prometheus/prometheus.service.ts b/src/common/prometheus/prometheus.service.ts index b5b19e79..ac371574 100644 --- a/src/common/prometheus/prometheus.service.ts +++ b/src/common/prometheus/prometheus.service.ts @@ -8,6 +8,9 @@ import { ConfigService } from 'common/config'; import { Metric, Options } from './interfaces'; import { METRICS_PREFIX, + METRIC_AVG_CHAIN_MISSED_REWARD, + METRIC_AVG_CHAIN_PENALTY, + METRIC_AVG_CHAIN_REWARD, METRIC_BUILD_INFO, METRIC_CHAIN_SYNC_PARTICIPATION_AVG_PERCENT, METRIC_CONTRACT_KEYS_TOTAL, @@ -18,6 +21,12 @@ import { METRIC_HIGH_REWARD_VALIDATOR_COUNT_MISS_PROPOSE, METRIC_HIGH_REWARD_VALIDATOR_COUNT_WITH_SYNC_PARTICIPATION_LESS_AVG_LAST_N_EPOCH, METRIC_OPERATOR_BALANCE_24H_DIFFERENCE, + METRIC_OPERATOR_CALCULATED_BALANCE_CALCULATION_ERROR, + METRIC_OPERATOR_CALCULATED_BALANCE_DELTA, + METRIC_OPERATOR_MISSED_REWARD, + METRIC_OPERATOR_PENALTY, + METRIC_OPERATOR_REAL_BALANCE_DELTA, + METRIC_OPERATOR_REWARD, METRIC_OPERATOR_SYNC_PARTICIPATION_AVG_PERCENT, METRIC_OTHER_SYNC_PARTICIPATION_AVG_PERCENT, METRIC_OTHER_VALIDATOR_COUNT_GOOD_PROPOSE, @@ -36,6 +45,7 @@ import { METRIC_TASK_DURATION_SECONDS, METRIC_TASK_RESULT_COUNT, METRIC_TOTAL_BALANCE_24H_DIFFERENCE, + METRIC_USER_OPERATORS_IDENTIFIES, METRIC_USER_SYNC_PARTICIPATION_AVG_PERCENT, METRIC_USER_VALIDATORS, METRIC_VALIDATORS, @@ -190,6 +200,12 @@ export class PrometheusService implements OnApplicationBootstrap { labelNames: ['name', 'status'], }); + public operatorsIdentifies = this.getOrCreateMetric('Gauge', { + name: METRIC_USER_OPERATORS_IDENTIFIES, + help: 'Operators identifies', + labelNames: ['nos_id', 'nos_name'], + }); + public validators = this.getOrCreateMetric('Gauge', { name: METRIC_VALIDATORS, help: 'Validators number', @@ -202,15 +218,33 @@ export class PrometheusService implements OnApplicationBootstrap { labelNames: ['nos_name', 'status'], }); - public validatorBalanceDelta = this.getOrCreateMetric('Gauge', { + public avgValidatorBalanceDelta = this.getOrCreateMetric('Gauge', { name: METRIC_VALIDATOR_BALANCES_DELTA, - help: 'validator balances delta', + help: 'average validator balances delta (6 epochs delta)', + labelNames: ['nos_name'], + }); + + public operatorRealBalanceDelta = this.getOrCreateMetric('Gauge', { + name: METRIC_OPERATOR_REAL_BALANCE_DELTA, + help: 'operator real balance delta (according to state)', + labelNames: ['nos_name'], + }); + + public operatorCalculatedBalanceDelta = this.getOrCreateMetric('Gauge', { + name: METRIC_OPERATOR_CALCULATED_BALANCE_DELTA, + help: 'operator calculated balance delta (according to calculated rewards and penalties)', + labelNames: ['nos_name'], + }); + + public operatorCalculatedBalanceCalculationError = this.getOrCreateMetric('Gauge', { + name: METRIC_OPERATOR_CALCULATED_BALANCE_CALCULATION_ERROR, + help: 'operator calculated balance delta calculation error by real balance change', labelNames: ['nos_name'], }); public validatorQuantile001BalanceDelta = this.getOrCreateMetric('Gauge', { name: METRIC_VALIDATOR_QUANTILE_001_BALANCES_DELTA, - help: 'validator 0.1% quantile balances delta', + help: 'validator 0.1% quantile balances delta (6 epochs delta)', labelNames: ['nos_name'], }); @@ -400,6 +434,42 @@ export class PrometheusService implements OnApplicationBootstrap { labelNames: ['nos_name'], }); + public avgChainReward = this.getOrCreateMetric('Gauge', { + name: METRIC_AVG_CHAIN_REWARD, + help: 'avg rewards for each duty', + labelNames: ['duty'], + }); + + public operatorReward = this.getOrCreateMetric('Gauge', { + name: METRIC_OPERATOR_REWARD, + help: 'rewards for each duty for each operator', + labelNames: ['nos_name', 'duty'], + }); + + public avgChainMissedReward = this.getOrCreateMetric('Gauge', { + name: METRIC_AVG_CHAIN_MISSED_REWARD, + help: 'avg missed rewards for each duty', + labelNames: ['duty'], + }); + + public operatorMissedReward = this.getOrCreateMetric('Gauge', { + name: METRIC_OPERATOR_MISSED_REWARD, + help: 'missed rewards for each duty for each operator', + labelNames: ['nos_name', 'duty'], + }); + + public avgChainPenalty = this.getOrCreateMetric('Gauge', { + name: METRIC_AVG_CHAIN_PENALTY, + help: 'avg penalty for each duty', + labelNames: ['duty'], + }); + + public operatorPenalty = this.getOrCreateMetric('Gauge', { + name: METRIC_OPERATOR_PENALTY, + help: 'operator penalty for each duty', + labelNames: ['nos_name', 'duty'], + }); + public contractKeysTotal = this.getOrCreateMetric('Gauge', { name: METRIC_CONTRACT_KEYS_TOTAL, help: 'Contract keys', diff --git a/src/duty/attestation/attestation.constants.ts b/src/duty/attestation/attestation.constants.ts new file mode 100644 index 00000000..ad09ebcb --- /dev/null +++ b/src/duty/attestation/attestation.constants.ts @@ -0,0 +1,76 @@ +// from https://eth2book.info/bellatrix/part2/incentives/rewards/ +const TIMELY_SOURCE_WEIGHT = 14; // Ws +const TIMELY_TARGET_WEIGHT = 26; // Wt +const TIMELY_HEAD_WEIGHT = 14; // Wh +const WEIGHT_DENOMINATOR = 64; // W sigma +export const ATT_REWARD_FOR_MISSED = { source: 0, target: 0, head: 0 }; +export const ATT_PENALTY_FOR_MISSED = { + source: TIMELY_SOURCE_WEIGHT / WEIGHT_DENOMINATOR, + target: TIMELY_TARGET_WEIGHT / WEIGHT_DENOMINATOR, + head: 0, +}; +export const MISSED_ATTESTATION = { + att_happened: false, + att_meta: { + included_in_block: undefined, + reward_per_increment: ATT_REWARD_FOR_MISSED, + penalty_per_increment: ATT_PENALTY_FOR_MISSED, + }, +}; + +// Rewards +export const timelySource = (att_inc_delay: number, att_valid_source: boolean): number => { + if (att_valid_source && att_inc_delay <= 5) return TIMELY_SOURCE_WEIGHT; + else return 0; +}; +export const timelyTarget = (att_inc_delay: number, att_valid_source: boolean, att_valid_target: boolean): number => { + if (att_valid_source && att_valid_target && att_inc_delay <= 32) return TIMELY_TARGET_WEIGHT; + else return 0; +}; +export const timelyHead = ( + att_inc_delay: number, + att_valid_source: boolean, + att_valid_target: boolean, + att_valid_head: boolean, +): number => { + if (att_valid_source && att_valid_target && att_valid_head && att_inc_delay == 1) return TIMELY_HEAD_WEIGHT; + else return 0; +}; +export const attestationRewards = ( + att_inc_delay: number, + att_valid_source: boolean, + att_valid_target: boolean, + att_valid_head: boolean, +) => { + return { + source: timelySource(att_inc_delay, att_valid_source) / WEIGHT_DENOMINATOR, + target: timelyTarget(att_inc_delay, att_valid_source, att_valid_target) / WEIGHT_DENOMINATOR, + head: timelyHead(att_inc_delay, att_valid_source, att_valid_target, att_valid_head) / WEIGHT_DENOMINATOR, + }; +}; + +// Penalties +const untimelySource = (att_inc_delay: number, att_valid_source: boolean): number => { + if (!timelySource(att_inc_delay, att_valid_source)) return TIMELY_SOURCE_WEIGHT; + else return 0; +}; +const untimelyTarget = (att_inc_delay: number, att_valid_source: boolean, att_valid_target: boolean): number => { + if (!timelyTarget(att_inc_delay, att_valid_source, att_valid_target)) return TIMELY_TARGET_WEIGHT; + else return 0; +}; +const untimelyHead = (att_inc_delay: number, att_valid_source: boolean, att_valid_target: boolean, att_valid_head: boolean): number => { + if (!timelyHead(att_inc_delay, att_valid_source, att_valid_target, att_valid_head)) return 0; + else return 0; +}; +export const attestationPenalties = ( + att_inc_delay: number, + att_valid_source: boolean, + att_valid_target: boolean, + att_valid_head: boolean, +) => { + return { + source: untimelySource(att_inc_delay, att_valid_source) / WEIGHT_DENOMINATOR, + target: untimelyTarget(att_inc_delay, att_valid_source, att_valid_target) / WEIGHT_DENOMINATOR, + head: untimelyHead(att_inc_delay, att_valid_source, att_valid_target, att_valid_head) / WEIGHT_DENOMINATOR, + }; +}; diff --git a/src/duty/attestation/attestation.metrics.ts b/src/duty/attestation/attestation.metrics.ts index 9355fdd8..f54867e8 100644 --- a/src/duty/attestation/attestation.metrics.ts +++ b/src/duty/attestation/attestation.metrics.ts @@ -57,79 +57,79 @@ export class AttestationMetrics { private async perfectAttestationsLastEpoch() { const result = await this.storage.getValidatorCountWithPerfectAttestationsLastEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountPerfectAttestation.set({ nos_name: operator.name }, operatorResult ? operatorResult.amount : 0); }); - const other = result.find((a) => a.val_nos_name == 'NULL'); + const other = result.find((a) => a.val_nos_id == null); this.prometheus.otherValidatorsCountPerfectAttestation.set(other ? other.amount : 0); } private async missedAttestationsLastEpoch() { const result = await this.storage.getValidatorCountWithMissedAttestationsLastEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountMissAttestation.set({ nos_name: operator.name }, operatorResult ? operatorResult.amount : 0); }); - const other = result.find((a) => a.val_nos_name == 'NULL'); + const other = result.find((a) => a.val_nos_id == null); this.prometheus.otherValidatorsCountMissAttestation.set(other ? other.amount : 0); } private async highIncDelayAttestationsLastEpoch() { const result = await this.storage.getValidatorCountWithHighIncDelayAttestationsLastEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestation.set( { nos_name: operator.name, reason: BadAttReason.HighIncDelay }, operatorResult ? operatorResult.amount : 0, ); }); - const other = result.find((a) => a.val_nos_name == 'NULL'); + const other = result.find((a) => a.val_nos_id == null); this.prometheus.otherValidatorsCountInvalidAttestation.set({ reason: BadAttReason.HighIncDelay }, other ? other.amount : 0); } private async invalidHeadAttestationsLastEpoch() { const result = await this.storage.getValidatorCountWithInvalidHeadAttestationsLastEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestation.set( { nos_name: operator.name, reason: BadAttReason.InvalidHead }, operatorResult ? operatorResult.amount : 0, ); }); - const other = result.find((a) => a.val_nos_name == 'NULL'); + const other = result.find((a) => a.val_nos_id == null); this.prometheus.otherValidatorsCountInvalidAttestation.set({ reason: BadAttReason.InvalidHead }, other ? other.amount : 0); } private async invalidTargetAttestationsLastEpoch() { const result = await this.storage.getValidatorCountWithInvalidTargetAttestationsLastEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestation.set( { nos_name: operator.name, reason: BadAttReason.InvalidTarget }, operatorResult ? operatorResult.amount : 0, ); }); - const other = result.find((a) => a.val_nos_name == 'NULL'); + const other = result.find((a) => a.val_nos_id == null); this.prometheus.otherValidatorsCountInvalidAttestation.set({ reason: BadAttReason.InvalidTarget }, other ? other.amount : 0); } private async invalidSourceAttestationsLastEpoch() { const result = await this.storage.getValidatorCountWithInvalidSourceAttestationsLastEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestation.set( { nos_name: operator.name, reason: BadAttReason.InvalidSource }, operatorResult ? operatorResult.amount : 0, ); }); - const other = result.find((a) => a.val_nos_name == 'NULL'); + const other = result.find((a) => a.val_nos_id == null); this.prometheus.otherValidatorsCountInvalidAttestation.set({ reason: BadAttReason.InvalidSource }, other ? other.amount : 0); } private async missAttestationsLastNEpoch() { const result = await this.storage.getValidatorCountWithMissedAttestationsLastNEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountMissAttestationLastNEpoch.set( { nos_name: operator.name, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -140,7 +140,7 @@ export class AttestationMetrics { private async highIncDelayAttestationsLastNEpoch() { const result = await this.storage.getValidatorCountIncDelayGtOneAttestationsLastNEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestationLastNEpoch.set( { nos_name: operator.name, reason: BadAttReason.HighIncDelay, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -151,7 +151,7 @@ export class AttestationMetrics { private async invalidHeadAttestationsLastNEpoch() { const result = await this.storage.getValidatorCountWithInvalidHeadAttestationsLastNEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestationLastNEpoch.set( { nos_name: operator.name, reason: BadAttReason.InvalidHead, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -162,7 +162,7 @@ export class AttestationMetrics { private async invalidTargetAttestationsLastNEpoch() { const result = await this.storage.getValidatorCountWithInvalidTargetAttestationsLastNEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestationLastNEpoch.set( { nos_name: operator.name, reason: BadAttReason.InvalidTarget, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -173,7 +173,7 @@ export class AttestationMetrics { private async invalidSourceAttestationsLastNEpoch() { const result = await this.storage.getValidatorCountWithInvalidSourceAttestationsLastNEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestationLastNEpoch.set( { nos_name: operator.name, reason: BadAttReason.InvalidSource, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -184,7 +184,7 @@ export class AttestationMetrics { private async highAvgIncDelayAttestationsOfNEpoch() { const result = await this.storage.getValidatorCountHighAvgIncDelayAttestationOfNEpochQuery(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountHighAvgIncDelayAttestationOfNEpoch.set( { nos_name: operator.name, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -195,7 +195,7 @@ export class AttestationMetrics { private async incDelayGtTwoAttestationsLastNEpoch() { const result = await this.storage.getValidatorCountIncDelayGtTwoAttestationsLastNEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountHighIncDelayAttestationLastNEpoch.set( { nos_name: operator.name, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -206,7 +206,7 @@ export class AttestationMetrics { private async invalidAttestationPropertyGtOneLastNEpoch() { const result = await this.storage.getValidatorCountWithInvalidAttestationsPropertyGtOneLastNEpoch(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id != null && +a.val_nos_id == operator.index); this.prometheus.validatorsCountInvalidAttestationPropertyLastNEpoch.set( { nos_name: operator.name, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -222,7 +222,7 @@ export class AttestationMetrics { possibleHighRewardValidators, ); this.operators.forEach((operator) => { - const operatorResult = result.find((a) => a.val_nos_name == operator.name); + const operatorResult = result.find((a) => a.val_nos_id == operator.index); this.prometheus.highRewardValidatorsCountMissAttestationLastNEpoch.set( { nos_name: operator.name, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, diff --git a/src/duty/attestation/attestation.module.ts b/src/duty/attestation/attestation.module.ts index 479b321c..06106fe9 100644 --- a/src/duty/attestation/attestation.module.ts +++ b/src/duty/attestation/attestation.module.ts @@ -6,11 +6,12 @@ import { ClickhouseModule } from 'storage/clickhouse'; import { SummaryModule } from '../summary'; import { AttestationMetrics } from './attestation.metrics'; +import { AttestationRewards } from './attestation.rewards'; import { AttestationService } from './attestation.service'; @Module({ imports: [RegistryModule, ConsensusProviderModule, ClickhouseModule, SummaryModule], - providers: [AttestationService, AttestationMetrics], - exports: [AttestationService, AttestationMetrics], + providers: [AttestationService, AttestationMetrics, AttestationRewards], + exports: [AttestationService, AttestationMetrics, AttestationRewards], }) export class AttestationModule {} diff --git a/src/duty/attestation/attestation.rewards.ts b/src/duty/attestation/attestation.rewards.ts new file mode 100644 index 00000000..4f3b45f8 --- /dev/null +++ b/src/duty/attestation/attestation.rewards.ts @@ -0,0 +1,58 @@ +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; +import { Inject, Injectable, LoggerService } from '@nestjs/common'; + +import { ConfigService } from 'common/config'; +import { PrometheusService } from 'common/prometheus'; + +import { SummaryService } from '../summary'; +import { attestationRewards } from './attestation.constants'; + +@Injectable() +export class AttestationRewards { + prevEpoch: any; + public constructor( + @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, + protected readonly config: ConfigService, + protected readonly prometheus: PrometheusService, + protected readonly summary: SummaryService, + ) {} + + public async calculate(epoch: bigint) { + const epochMeta = this.summary.getMeta(); + // Attestation reward multipliers + const sourceParticipation = + Number(epochMeta.attestation.participation.source) / Number(epochMeta.state.active_validators_total_increments); + const targetParticipation = + Number(epochMeta.attestation.participation.target) / Number(epochMeta.state.active_validators_total_increments); + const headParticipation = Number(epochMeta.attestation.participation.head) / Number(epochMeta.state.active_validators_total_increments); + // Perfect attestation (with multipliers). Need for calculating missed reward + const perfect = attestationRewards(1, true, true, true); + const perfectAttestationRewards = BigInt( + Math.trunc(perfect.source * epochMeta.state.base_reward * 32 * sourceParticipation) + + Math.trunc(perfect.target * epochMeta.state.base_reward * 32 * targetParticipation) + + Math.trunc(perfect.head * epochMeta.state.base_reward * 32 * headParticipation), + ); + for (const v of this.summary.values()) { + if (!v.att_meta) continue; + const increments = Number(BigInt(v.val_effective_balance) / BigInt(10 ** 9)); + let att_earned_reward = 0n; + let att_missed_reward = 0n; + let att_penalty = 0n; + const rewardSource = Math.trunc(v.att_meta.reward_per_increment.source * epochMeta.state.base_reward * increments); + const rewardTarget = Math.trunc(v.att_meta.reward_per_increment.target * epochMeta.state.base_reward * increments); + const rewardHead = Math.trunc(v.att_meta.reward_per_increment.head * epochMeta.state.base_reward * increments); + const penaltySource = Math.trunc(v.att_meta.penalty_per_increment.source * epochMeta.state.base_reward * increments); + const penaltyTarget = Math.trunc(v.att_meta.penalty_per_increment.target * epochMeta.state.base_reward * increments); + const penaltyHead = Math.trunc(v.att_meta.penalty_per_increment.head * epochMeta.state.base_reward * increments); + att_earned_reward = BigInt( + Math.trunc(rewardSource * sourceParticipation) + + Math.trunc(rewardTarget * targetParticipation) + + Math.trunc(rewardHead * headParticipation), + ); + att_missed_reward = perfectAttestationRewards - att_earned_reward; + att_penalty = BigInt(penaltySource + penaltyTarget + penaltyHead); + this.summary.set(v.val_id, { epoch, val_id: v.val_id, att_earned_reward, att_missed_reward, att_penalty }); + } + return true; + } +} diff --git a/src/duty/attestation/attestation.service.ts b/src/duty/attestation/attestation.service.ts index 71b39a43..aa2d4423 100644 --- a/src/duty/attestation/attestation.service.ts +++ b/src/duty/attestation/attestation.service.ts @@ -1,6 +1,10 @@ -import { BitVectorType, fromHexString } from '@chainsafe/ssz'; +import { BitArray, BitVectorType, fromHexString } from '@chainsafe/ssz'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { chain } from 'stream-chain'; +import { parser } from 'stream-json'; +import { pick } from 'stream-json/filters/Pick'; +import { streamArray } from 'stream-json/streamers/StreamArray'; import { ConfigService } from 'common/config'; import { BlockInfoResponse, ConsensusProviderService } from 'common/eth-providers'; @@ -8,10 +12,11 @@ import { bigintRange } from 'common/functions/range'; import { PrometheusService, TrackTask } from 'common/prometheus'; import { SummaryService } from '../summary'; +import { MISSED_ATTESTATION, attestationPenalties, attestationRewards } from './attestation.constants'; interface SlotAttestation { included_in_block: bigint; - bits: boolean[]; + bits: BitArray; head: string; target_root: string; target_epoch: bigint; @@ -23,6 +28,7 @@ interface SlotAttestation { @Injectable() export class AttestationService { + private processedEpoch: bigint; private readonly slotsInEpoch: bigint; private readonly savedCanonSlotsAttProperties: Map; public constructor( @@ -38,44 +44,62 @@ export class AttestationService { @TrackTask('check-attestation-duties') public async check(epoch: bigint, stateSlot: bigint): Promise { + this.processedEpoch = epoch; this.savedCanonSlotsAttProperties.clear(); - const { attestations, allMissedSlots } = await this.getProcessedAttestations(epoch); + const { attestations } = await this.getProcessedAttestations(); this.logger.log(`Getting attestation duties info`); - const committees = (await this.clClient.getAttestationCommitteesInfo(stateSlot, epoch)).map((c) => ({ - index: Number(c.index), - slot: BigInt(c.slot), - validators: c.validators.map((v) => BigInt(v)), - })); + const committees = await this.getAttestationCommittees(stateSlot); this.logger.log(`Processing attestation duty info`); - for (const committee of committees) { - for (const attestation of attestations) { - if ( - attestation.slot != committee.slot || - attestation.committee_index != committee.index || - attestation.included_in_block <= committee.slot - ) - continue; - const missedSlotsCount = allMissedSlots.filter( - (missed) => missed > committee.slot && missed < attestation.included_in_block, - ).length; - const att_inc_delay = Number(attestation.included_in_block - committee.slot) - missedSlotsCount; - const [canonHead, canonTarget, canonSource] = await Promise.all([ - this.getCanonSlotRoot(attestation.slot), - this.getCanonSlotRoot(attestation.target_epoch * this.slotsInEpoch), - this.getCanonSlotRoot(attestation.source_epoch * this.slotsInEpoch), - ]); - const att_valid_head = attestation.head == canonHead; - const att_valid_target = attestation.target_root == canonTarget; - const att_valid_source = attestation.source_root == canonSource; - for (const [valCommIndex, validatorIndex] of committee.validators.entries()) { - if (this.summary.get(validatorIndex)?.att_happened) continue; - const att_happened = attestation.bits[valCommIndex]; - let summary = { epoch, val_id: validatorIndex, att_happened }; - const properties = { att_inc_delay, att_valid_head, att_valid_source, att_valid_target }; - if (att_happened) summary = { ...summary, ...properties }; - this.summary.set(validatorIndex, summary); - } + for (const attestation of attestations) { + // Each attestation corresponds to committee. Committee may have several aggregate attestations + const committee = committees.get(`${attestation.committee_index}_${attestation.slot}`); + if (!committee) continue; + await this.processAttestation(attestation, committee); + await new Promise((resolve) => { + // Long loop (2048 committees will be checked by ~7k attestations). + // We need to unblock event loop immediately after each iteration + // It makes this cycle slower but safer (but since it is executed async, impact will be minimal) + // If we don't do this, it can freeze scraping Prometheus metrics and other important operations + // Source: https://snyk.io/blog/nodejs-how-even-quick-async-functions-can-block-the-event-loop-starve-io/ + return setImmediate(() => resolve(true)); + }); + } + } + + protected async processAttestation(attestation: SlotAttestation, committee: bigint[]) { + const [canonHead, canonTarget, canonSource] = await Promise.all([ + this.getCanonSlotRoot(attestation.slot), + this.getCanonSlotRoot(attestation.target_epoch * this.slotsInEpoch), + this.getCanonSlotRoot(attestation.source_epoch * this.slotsInEpoch), + ]); + const att_valid_head = attestation.head == canonHead; + const att_valid_target = attestation.target_root == canonTarget; + const att_valid_source = attestation.source_root == canonSource; + const att_inc_delay = Number(attestation.included_in_block - attestation.slot); + const reward_per_increment = attestationRewards(att_inc_delay, att_valid_source, att_valid_target, att_valid_head); + const penalty_per_increment = attestationPenalties(att_inc_delay, att_valid_source, att_valid_target, att_valid_head); + for (const [valCommIndex, validatorIndex] of committee.entries()) { + const attHappened = this.summary.get(validatorIndex)?.att_happened; + if (attHappened == true) continue; // already processed validator. it was in one of previous attestation + const att_happened = attestation.bits.get(valCommIndex); + if (!att_happened) { + this.summary.set(validatorIndex, { epoch: this.processedEpoch, val_id: validatorIndex, ...MISSED_ATTESTATION }); + continue; } + this.summary.set(validatorIndex, { + val_id: validatorIndex, + epoch: this.processedEpoch, + att_happened: true, + att_inc_delay, + att_valid_head, + att_valid_source, + att_valid_target, + att_meta: { + included_in_block: attestation.included_in_block, + reward_per_increment, + penalty_per_increment, + }, + }); } } @@ -87,14 +111,17 @@ export class AttestationService { return root; } - protected async getProcessedAttestations(epoch: bigint) { + @TrackTask('process-chain-attestations') + protected async getProcessedAttestations() { this.logger.log(`Processing attestations from blocks info`); + // todo: Should we check orphaned blocks and how? eg. https://beaconcha.in/slot/5306177 + const bitsMap = new Map(); const attestations: SlotAttestation[] = []; let allMissedSlots: bigint[] = []; let lastBlockInfo: BlockInfoResponse | undefined; let lastMissedSlots: bigint[]; // Check all slots from epoch start to last epoch slot + 32 (max inclusion delay) - const firstSlotInEpoch = epoch * this.slotsInEpoch; + const firstSlotInEpoch = this.processedEpoch * this.slotsInEpoch; const slotsToCheck: bigint[] = bigintRange(firstSlotInEpoch, firstSlotInEpoch + this.slotsInEpoch * 2n); for (const slotToCheck of slotsToCheck) { if (lastBlockInfo && lastBlockInfo.message.slot > slotToCheck.toString()) { @@ -106,11 +133,16 @@ export class AttestationService { continue; // Failed to get info about the nearest existing block } for (const att of lastBlockInfo.message.body.attestations) { - const bytesArray = fromHexString(att.aggregation_bits); - const CommitteeBits = new BitVectorType(bytesArray.length * 8); + let bits = bitsMap.get(att.aggregation_bits); + if (!bits) { + const bytesArray = fromHexString(att.aggregation_bits); + const CommitteeBits = new BitVectorType(bytesArray.length * 8); + bits = CommitteeBits.deserialize(bytesArray); + bitsMap.set(att.aggregation_bits, bits); + } attestations.push({ included_in_block: BigInt(lastBlockInfo.message.slot), - bits: CommitteeBits.deserialize(bytesArray).toBoolArray(), + bits: bits, head: att.data.beacon_block_root, target_root: att.data.target.root, target_epoch: BigInt(att.data.target.epoch), @@ -124,4 +156,24 @@ export class AttestationService { this.logger.debug(`All missed slots in getting attestations info process: ${allMissedSlots}`); return { attestations, allMissedSlots }; } + + @TrackTask('get-attestation-committees') + protected async getAttestationCommittees(stateSlot: bigint): Promise> { + const readStream = await this.clClient.getAttestationCommitteesInfo(stateSlot, this.processedEpoch); + const committees = new Map(); + const pipeline = chain([readStream, parser(), pick({ filter: 'data' }), streamArray(), (data) => data.value]); + const streamTask = async () => + new Promise((resolve, reject) => { + pipeline.on('data', (committee) => + committees.set( + `${committee.index}_${committee.slot}`, + committee.validators.map((v) => BigInt(v)), + ), + ); + pipeline.on('error', (error) => reject(error)); + pipeline.on('end', () => resolve(true)); + }); + await streamTask().finally(() => pipeline.destroy()); + return committees; + } } diff --git a/src/duty/attestation/index.ts b/src/duty/attestation/index.ts index 5974c7cc..9fc56cdc 100644 --- a/src/duty/attestation/index.ts +++ b/src/duty/attestation/index.ts @@ -1,3 +1,4 @@ export * from './attestation.module'; export * from './attestation.service'; +export * from './attestation.rewards'; export * from './attestation.metrics'; diff --git a/src/duty/duty.metrics.ts b/src/duty/duty.metrics.ts index 50109bdf..a1633e44 100644 --- a/src/duty/duty.metrics.ts +++ b/src/duty/duty.metrics.ts @@ -2,9 +2,10 @@ import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { ConfigService } from 'common/config'; -import { BlockHeaderResponse, ConsensusProviderService } from 'common/eth-providers'; +import { ConsensusProviderService } from 'common/eth-providers'; import { PrometheusService, TrackTask } from 'common/prometheus'; +import { ClickhouseService } from '../storage'; import { AttestationMetrics } from './attestation'; import { ProposeMetrics } from './propose'; import { StateMetrics } from './state'; @@ -24,6 +25,7 @@ export class DutyMetrics { protected readonly proposeMetrics: ProposeMetrics, protected readonly syncMetrics: SyncMetrics, protected readonly summaryMetrics: SummaryMetrics, + protected readonly storage: ClickhouseService, ) {} @TrackTask('calc-all-duties-metrics') @@ -32,6 +34,7 @@ export class DutyMetrics { await Promise.all([this.withPossibleHighReward(epoch, possibleHighRewardValidators), this.stateMetrics.calculate(epoch)]); // we must calculate summary metrics after all duties to avoid errors in processing await this.summaryMetrics.calculate(epoch); + await this.storage.updateEpochProcessing({ epoch, is_calculated: true }); } private async withPossibleHighReward(epoch: bigint, possibleHighRewardValidators: string[]): Promise { @@ -41,17 +44,4 @@ export class DutyMetrics { this.syncMetrics.calculate(epoch, possibleHighRewardValidators), ]); } - - @TrackTask('high-reward-validators') - public async getPossibleHighRewardValidators(): Promise { - const actualSlotHeader = await this.clClient.getBlockHeader('head'); - const headEpoch = BigInt(actualSlotHeader.header.message.slot) / BigInt(this.config.get('FETCH_INTERVAL_SLOTS')); - this.logger.log('Getting possible high reward validator indexes'); - const propDependentRoot = await this.clClient.getDutyDependentRoot(headEpoch); - const [sync, prop] = await Promise.all([ - this.clClient.getSyncCommitteeInfo('finalized', headEpoch), - this.clClient.getCanonicalProposerDuties(headEpoch, propDependentRoot), - ]); - return [...new Set([...prop.map((v) => v.validator_index), ...sync.validators])]; - } } diff --git a/src/duty/duty.module.ts b/src/duty/duty.module.ts index a3004fc8..7f3fee03 100644 --- a/src/duty/duty.module.ts +++ b/src/duty/duty.module.ts @@ -5,13 +5,14 @@ import { BlockCacheModule } from 'common/eth-providers/consensus-provider/block- import { RegistryModule } from 'common/validators-registry'; import { ClickhouseModule } from 'storage/clickhouse'; -import { AttestationModule, AttestationService } from './attestation'; +import { AttestationModule } from './attestation'; import { DutyMetrics } from './duty.metrics'; +import { DutyRewards } from './duty.rewards'; import { DutyService } from './duty.service'; -import { ProposeModule, ProposeService } from './propose'; -import { StateModule, StateService } from './state'; -import { SummaryModule, SummaryService } from './summary'; -import { SyncModule, SyncService } from './sync'; +import { ProposeModule } from './propose'; +import { StateModule } from './state'; +import { SummaryModule } from './summary'; +import { SyncModule } from './sync'; @Module({ imports: [ @@ -25,7 +26,7 @@ import { SyncModule, SyncService } from './sync'; ClickhouseModule, RegistryModule, ], - providers: [DutyService, DutyMetrics, AttestationService, ProposeService, StateService, SyncService, SummaryService], - exports: [DutyService, DutyMetrics, AttestationService, ProposeService, StateService, SyncService, SummaryService], + providers: [DutyService, DutyMetrics, DutyRewards], + exports: [DutyService, DutyMetrics], }) export class DutyModule {} diff --git a/src/duty/duty.rewards.ts b/src/duty/duty.rewards.ts new file mode 100644 index 00000000..8d5a2cc9 --- /dev/null +++ b/src/duty/duty.rewards.ts @@ -0,0 +1,32 @@ +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; +import { Inject, Injectable, LoggerService } from '@nestjs/common'; + +import { ConfigService } from 'common/config'; +import { PrometheusService, TrackTask } from 'common/prometheus'; + +import { AttestationRewards } from './attestation'; +import { ProposeRewards } from './propose'; +import { EpochMeta } from './summary'; +import { SyncRewards } from './sync'; + +@Injectable() +export class DutyRewards { + public constructor( + @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, + protected readonly config: ConfigService, + protected readonly prometheus: PrometheusService, + + protected readonly attestationRewards: AttestationRewards, + protected readonly syncRewards: SyncRewards, + protected readonly proposerRewards: ProposeRewards, + ) {} + + @TrackTask('calc-all-duties-rewards') + public async calculate(epoch: bigint, prevEpochMetadata: EpochMeta) { + // todo: 'Slashed' case + // todo: 'Inactivity leak' case + await Promise.all([this.attestationRewards.calculate(epoch), this.syncRewards.calculate(epoch)]); + // should be calculated based on attestation and sync rewards + await this.proposerRewards.calculate(epoch, prevEpochMetadata); + } +} diff --git a/src/duty/duty.service.ts b/src/duty/duty.service.ts index 9e15d787..468f050b 100644 --- a/src/duty/duty.service.ts +++ b/src/duty/duty.service.ts @@ -2,17 +2,20 @@ import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { ConfigService } from 'common/config'; -import { ConsensusProviderService } from 'common/eth-providers'; +import { BlockHeaderResponse, ConsensusProviderService } from 'common/eth-providers'; import { BlockCacheService } from 'common/eth-providers/consensus-provider/block-cache'; import { bigintRange } from 'common/functions/range'; import { PrometheusService, TrackTask } from 'common/prometheus'; import { ClickhouseService } from 'storage'; import { AttestationService } from './attestation'; +import { DutyRewards } from './duty.rewards'; import { ProposeService } from './propose'; +import { PROPOSER_WEIGHT, WEIGHT_DENOMINATOR } from './propose/propose.constants'; import { StateService } from './state'; import { SummaryService } from './summary'; import { SyncService } from './sync'; +import { syncReward } from './sync/sync.constants'; @Injectable() export class DutyService { @@ -29,20 +32,29 @@ export class DutyService { protected readonly sync: SyncService, protected readonly summary: SummaryService, protected readonly storage: ClickhouseService, + protected readonly rewards: DutyRewards, ) {} - public async checkAndWrite(epoch: bigint, stateSlot: bigint): Promise { + public async checkAndWrite(epoch: bigint, stateSlot: bigint): Promise { // Prefetch will be done before main checks because duty by state requests are heavy // and while we wait for their responses we fetch blocks and headers. // If for some reason prefetch task will be slower than duty by state requests, // blocks and headers will be fetched inside tasks of checks - await Promise.all([this.prefetch(epoch), this.checkAll(epoch, stateSlot)]); - await this.write(); + const [, , possibleHighRewardVals] = await Promise.all([ + this.prefetch(epoch), + this.checkAll(epoch, stateSlot), + this.getPossibleHighRewardValidators(), + ]); + await this.writeEpochMeta(epoch); + await this.writeSummary(); + await this.storage.updateEpochProcessing({ epoch, is_stored: true }); + return possibleHighRewardVals; } @TrackTask('check-all-duties') protected async checkAll(epoch: bigint, stateSlot: bigint): Promise { this.summary.clear(); + this.summary.clearMeta(); this.logger.log('Checking duties of validators'); await Promise.all([ this.state.check(epoch, stateSlot), @@ -50,6 +62,11 @@ export class DutyService { this.sync.check(epoch, stateSlot), this.propose.check(epoch), ]); + // must be done after all duties check + await this.fillCurrentEpochMetadata(); + // calculate rewards after check all duties + const prevEpochMetadata = await this.storage.getEpochMetadata(epoch - 1n); + await this.rewards.calculate(epoch, prevEpochMetadata); } @TrackTask('prefetch-slots') @@ -61,14 +78,77 @@ export class DutyService { const slots: bigint[] = bigintRange(firstSlotInEpoch, firstSlotInEpoch + slotsInEpoch * 2n); const toFetch = slots.map((s) => [this.clClient.getBlockHeader(s), this.clClient.getBlockInfo(s)]).flat(); while (toFetch.length > 0) { - const chunk = toFetch.splice(0, 64); + const chunk = toFetch.splice(0, 32); await Promise.all(chunk); } } - protected async write(): Promise { + @TrackTask('high-reward-validators') + public async getPossibleHighRewardValidators(): Promise { + const actualSlotHeader = await this.clClient.getBlockHeader('head'); + const headEpoch = BigInt(actualSlotHeader.header.message.slot) / BigInt(this.config.get('FETCH_INTERVAL_SLOTS')); + this.logger.log('Getting possible high reward validator indexes'); + const propDependentRoot = await this.clClient.getDutyDependentRoot(headEpoch); + const [sync, prop] = await Promise.all([ + this.clClient.getSyncCommitteeInfo('finalized', headEpoch), + this.clClient.getCanonicalProposerDuties(headEpoch, propDependentRoot), + ]); + return [...new Set([...prop.map((v) => v.validator_index), ...sync.validators])]; + } + + @TrackTask('fill-epoch-metadata') + protected async fillCurrentEpochMetadata() { + const meta = this.summary.getMeta(); + meta.attestation = { + participation: { source: 0n, target: 0n, head: 0n }, + blocks_rewards: new Map(), + }; + meta.sync.blocks_rewards = new Map(); + // block can be with zero synchronization + meta.sync.blocks_to_sync.forEach((b) => meta.sync.blocks_rewards.set(b, 0n)); + meta.sync.per_block_reward = syncReward(meta.state.active_validators_total_increments, meta.state.base_reward); + const perSyncProposerReward = Math.trunc( + (Number(meta.sync.per_block_reward) * PROPOSER_WEIGHT) / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT), + ); + for (const v of this.summary.values()) { + // todo: maybe data consistency checks are needed. e.g. attested validator must have an effective balance + if (v.att_meta && v.att_meta.included_in_block) { + let rewards = meta.attestation.blocks_rewards.get(v.att_meta.included_in_block) ?? 0n; + const effectiveBalance = BigInt(v.val_effective_balance ?? 0n); + const increments = Number(effectiveBalance / BigInt(10 ** 9)); + if (v.att_meta?.reward_per_increment.source != 0) { + meta.attestation.participation.source += effectiveBalance / BigInt(10 ** 9); + rewards += BigInt(Math.trunc(meta.state.base_reward * increments * v.att_meta.reward_per_increment.source)); + } + if (v.att_meta?.reward_per_increment.target != 0) { + meta.attestation.participation.target += effectiveBalance / BigInt(10 ** 9); + rewards += BigInt(Math.trunc(meta.state.base_reward * increments * v.att_meta.reward_per_increment.target)); + } + if (v.att_meta?.reward_per_increment.head != 0) { + meta.attestation.participation.head += effectiveBalance / BigInt(10 ** 9); + rewards += BigInt(Math.trunc(meta.state.base_reward * increments * v.att_meta.reward_per_increment.head)); + } + meta.attestation.blocks_rewards.set(v.att_meta.included_in_block, rewards); + } + if (v.is_sync) { + for (const block of v.sync_meta.synced_blocks) { + meta.sync.blocks_rewards.set(block, meta.sync.blocks_rewards.get(block) + BigInt(perSyncProposerReward)); + } + } + } + this.summary.setMeta(meta); + } + + protected async writeSummary(): Promise { this.logger.log('Writing summary of duties into DB'); - await this.storage.writeSummary(this.summary.values()); + await this.storage.writeSummary(this.summary.valuesToWrite()); this.summary.clear(); } + + protected async writeEpochMeta(epoch: bigint): Promise { + this.logger.log('Writing epoch metadata into DB'); + const meta = this.summary.getMeta(); + await this.storage.writeEpochMeta(epoch, meta); + this.summary.clearMeta(); + } } diff --git a/src/duty/propose/index.ts b/src/duty/propose/index.ts index ac84317a..c7a9f6cd 100644 --- a/src/duty/propose/index.ts +++ b/src/duty/propose/index.ts @@ -1,3 +1,4 @@ export * from './propose.module'; export * from './propose.service'; export * from './propose.metrics'; +export * from './propose.rewards'; diff --git a/src/duty/propose/propose.constants.ts b/src/duty/propose/propose.constants.ts new file mode 100644 index 00000000..994e6a91 --- /dev/null +++ b/src/duty/propose/propose.constants.ts @@ -0,0 +1,9 @@ +// from https://eth2book.info/bellatrix/part2/incentives/rewards/ +export const PROPOSER_WEIGHT = 8; // Wp +export const WEIGHT_DENOMINATOR = 64; // W sigma + +export const proposerAttPartReward = ( + r: bigint, // total rewards to the attesters in this block +) => { + return (BigInt(PROPOSER_WEIGHT) * r) / BigInt(WEIGHT_DENOMINATOR - PROPOSER_WEIGHT); +}; diff --git a/src/duty/propose/propose.metrics.ts b/src/duty/propose/propose.metrics.ts index 399bc10a..6048e621 100644 --- a/src/duty/propose/propose.metrics.ts +++ b/src/duty/propose/propose.metrics.ts @@ -29,20 +29,20 @@ export class ProposeMetrics { private async goodProposes() { const result = await this.storage.getValidatorsCountWithGoodProposes(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((p) => p.val_nos_name == operator.name); + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); this.prometheus.validatorsCountGoodPropose.set({ nos_name: operator.name }, operatorResult ? operatorResult.amount : 0); }); - const other = result.find((p) => p.val_nos_name == 'NULL'); + const other = result.find((p) => p.val_nos_id == null); this.prometheus.otherValidatorsCountGoodPropose.set(other ? other.amount : 0); } private async missProposes() { const result = await this.storage.getValidatorsCountWithMissedProposes(this.processedEpoch); this.operators.forEach((operator) => { - const operatorResult = result.find((p) => p.val_nos_name == operator.name); + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); this.prometheus.validatorsCountMissPropose.set({ nos_name: operator.name }, operatorResult ? operatorResult.amount : 0); }); - const other = result.find((p) => p.val_nos_name == 'NULL'); + const other = result.find((p) => p.val_nos_id == null); this.prometheus.otherValidatorsCountMissPropose.set(other ? other.amount : 0); } @@ -51,7 +51,7 @@ export class ProposeMetrics { if (possibleHighRewardValidators.length > 0) result = await this.storage.getValidatorsCountWithMissedProposes(this.processedEpoch, possibleHighRewardValidators); this.operators.forEach((operator) => { - const operatorResult = result.find((p) => p.val_nos_name == operator.name); + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); this.prometheus.highRewardValidatorsCountMissPropose.set({ nos_name: operator.name }, operatorResult ? operatorResult.amount : 0); }); } diff --git a/src/duty/propose/propose.module.ts b/src/duty/propose/propose.module.ts index 536ab1a5..5e44214f 100644 --- a/src/duty/propose/propose.module.ts +++ b/src/duty/propose/propose.module.ts @@ -6,11 +6,12 @@ import { ClickhouseModule } from 'storage/clickhouse'; import { SummaryModule } from '../summary'; import { ProposeMetrics } from './propose.metrics'; +import { ProposeRewards } from './propose.rewards'; import { ProposeService } from './propose.service'; @Module({ imports: [RegistryModule, ConsensusProviderModule, ClickhouseModule, SummaryModule], - providers: [ProposeService, ProposeMetrics], - exports: [ProposeService, ProposeMetrics], + providers: [ProposeService, ProposeMetrics, ProposeRewards], + exports: [ProposeService, ProposeMetrics, ProposeRewards], }) export class ProposeModule {} diff --git a/src/duty/propose/propose.rewards.ts b/src/duty/propose/propose.rewards.ts new file mode 100644 index 00000000..b2ee0676 --- /dev/null +++ b/src/duty/propose/propose.rewards.ts @@ -0,0 +1,75 @@ +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; +import { Inject, Injectable, LoggerService } from '@nestjs/common'; + +import { ConfigService } from 'common/config'; +import { PrometheusService } from 'common/prometheus'; + +import { EpochMeta, SummaryService } from '../summary'; +import { proposerAttPartReward } from './propose.constants'; + +@Injectable() +export class ProposeRewards { + public constructor( + @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, + protected readonly config: ConfigService, + protected readonly prometheus: PrometheusService, + protected readonly summary: SummaryService, + ) {} + + public async calculate(epoch: bigint, prevEpochMetadata: EpochMeta) { + let attestationsSumOfSum = 0n; + let syncSumOfSum = 0n; + // Merge attestations metadata from two epochs + // It's needed to calculate rewards of checkpoint block. Because first block of epoch contains attestations from previous + // At the first app start it is possible that reward for such block will not be calculated, + // because there is no metadata of the previous epoch + const blocksAttestationsRewardSum = new Map(); + const prevEpoch = prevEpochMetadata.attestation?.blocks_rewards ?? new Map(); + if (prevEpoch.size == 0) { + this.logger.warn( + "Proposal reward will not be calculated accurately because previous epoch's metadata does not exist. " + + "Probably, it's the first run of application", + ); + } + const currEpoch = this.summary.getMeta().attestation.blocks_rewards; + for (const block of new Map([...prevEpoch.entries(), ...currEpoch.entries()]).keys()) { + let merged = 0n; + const prev = prevEpoch.get(block) ?? 0n; + const curr = currEpoch.get(block) ?? 0n; + merged += prev + curr; + blocksAttestationsRewardSum.set(block, merged); + } + const blocksSyncRewardSum = this.summary.getMeta().sync.blocks_rewards; + + blocksAttestationsRewardSum.forEach((rewards) => (attestationsSumOfSum += rewards)); + const attestationsAvg = attestationsSumOfSum / BigInt(blocksAttestationsRewardSum.size); + + blocksSyncRewardSum.forEach((rewards) => (syncSumOfSum += rewards)); + const syncAvg = syncSumOfSum / BigInt(blocksSyncRewardSum.size); + + for (const v of this.summary.values()) { + if (!v.is_proposer) continue; + let propose_earned_reward = 0n; + let propose_missed_reward = 0n; + const propose_penalty = 0n; + if (v.block_proposed) { + const attRewardSum = blocksAttestationsRewardSum.get(v.block_to_propose); + const syncRewardSum = blocksSyncRewardSum.get(v.block_to_propose); + if (attRewardSum == undefined || syncRewardSum == undefined) { + this.logger.warn(`Can't calculate reward for block ${v.block_to_propose}. There is no metadata of previous epoch`); + continue; + } + propose_earned_reward = proposerAttPartReward(attRewardSum) + syncRewardSum; + } else { + propose_missed_reward = proposerAttPartReward(attestationsAvg) + syncAvg; + } + this.summary.set(v.val_id, { + epoch, + val_id: v.val_id, + propose_earned_reward, + propose_missed_reward, + propose_penalty, + }); + } + } +} diff --git a/src/duty/state/state.metrics.ts b/src/duty/state/state.metrics.ts index de31f4d2..573e6aa4 100644 --- a/src/duty/state/state.metrics.ts +++ b/src/duty/state/state.metrics.ts @@ -29,10 +29,11 @@ export class StateMetrics { this.processedEpoch = epoch; this.operators = await this.registryService.getOperators(); await Promise.all([ + this.operatorsIdentifies(), this.nosStats(), this.userValidatorsStats(), this.otherValidatorsStats(), - this.deltas(), + this.avgDeltas(), this.minDeltas(), this.negativeValidatorsCount(), this.totalBalance24hDifference(), @@ -41,31 +42,36 @@ export class StateMetrics { ]); } + private async operatorsIdentifies() { + this.operators.forEach((operator) => this.prometheus.operatorsIdentifies.set({ nos_id: operator.index, nos_name: operator.name }, 1)); + } + private async nosStats() { const result = await this.storage.getUserNodeOperatorsStats(this.processedEpoch); - for (const nosStat of result) { + this.operators.forEach((operator) => { + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); this.prometheus.userValidators.set( { - nos_name: nosStat.val_nos_name, + nos_name: operator.name, status: PrometheusValStatus.Slashed, }, - nosStat.slashed, + operatorResult ? operatorResult.slashed : 0, ); this.prometheus.userValidators.set( { - nos_name: nosStat.val_nos_name, + nos_name: operator.name, status: PrometheusValStatus.Ongoing, }, - nosStat.active_ongoing, + operatorResult ? operatorResult.active_ongoing : 0, ); this.prometheus.userValidators.set( { - nos_name: nosStat.val_nos_name, + nos_name: operator.name, status: PrometheusValStatus.Pending, }, - nosStat.pending, + operatorResult ? operatorResult.pending : 0, ); - } + }); } private async userValidatorsStats() { @@ -120,25 +126,30 @@ export class StateMetrics { ); } - private async deltas() { - const result = await this.storage.getValidatorBalancesDelta(this.processedEpoch); - for (const delta of result) { - this.prometheus.validatorBalanceDelta.set({ nos_name: delta.val_nos_name }, delta.delta); - } + private async avgDeltas() { + const result = await this.storage.getAvgValidatorBalanceDelta(this.processedEpoch); + this.operators.forEach((operator) => { + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); + this.prometheus.avgValidatorBalanceDelta.set({ nos_name: operator.name }, operatorResult ? operatorResult.delta : 0); + }); } private async minDeltas() { const result = await this.storage.getValidatorQuantile0001BalanceDeltas(this.processedEpoch); - for (const minDelta of result) { - this.prometheus.validatorQuantile001BalanceDelta.set({ nos_name: minDelta.val_nos_name }, minDelta.delta); - } + this.operators.forEach((operator) => { + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); + this.prometheus.validatorQuantile001BalanceDelta.set({ nos_name: operator.name }, operatorResult ? operatorResult.delta : 0); + }); } private async negativeValidatorsCount() { const result = await this.storage.getValidatorsCountWithNegativeDelta(this.processedEpoch); this.operators.forEach((operator) => { - const negDelta = result.find((d) => d.val_nos_name == operator.name); - this.prometheus.validatorsCountWithNegativeBalanceDelta.set({ nos_name: operator.name }, negDelta ? negDelta.neg_count : 0); + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); + this.prometheus.validatorsCountWithNegativeBalanceDelta.set( + { nos_name: operator.name }, + operatorResult ? operatorResult.neg_count : 0, + ); }); } @@ -151,8 +162,9 @@ export class StateMetrics { private async operatorBalance24hDifference() { const result = await this.storage.getOperatorBalance24hDifference(this.processedEpoch); - result.forEach((d) => { - this.prometheus.operatorBalance24hDifference.set({ nos_name: d.val_nos_name }, d.diff); + this.operators.forEach((operator) => { + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); + this.prometheus.operatorBalance24hDifference.set({ nos_name: operator.name }, operatorResult ? operatorResult.diff : 0); }); } diff --git a/src/duty/state/state.service.ts b/src/duty/state/state.service.ts index 5c4f8bd4..ee71240d 100644 --- a/src/duty/state/state.service.ts +++ b/src/duty/state/state.service.ts @@ -1,8 +1,12 @@ import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { chain } from 'stream-chain'; +import { parser } from 'stream-json'; +import { pick } from 'stream-json/filters/Pick'; +import { streamArray } from 'stream-json/streamers/StreamArray'; import { ConfigService } from 'common/config'; -import { ConsensusProviderService } from 'common/eth-providers'; +import { ConsensusProviderService, StateValidatorResponse, ValStatus } from 'common/eth-providers'; import { PrometheusService, TrackTask } from 'common/prometheus'; import { RegistryService } from 'common/validators-registry'; import { ClickhouseService } from 'storage/clickhouse'; @@ -26,10 +30,17 @@ export class StateService { const slotTime = await this.clClient.getSlotTime(epoch * BigInt(this.config.get('FETCH_INTERVAL_SLOTS'))); const keysIndexed = await this.registry.getActualKeysIndexed(Number(slotTime)); this.logger.log('Getting all validators state'); - const states = await this.clClient.getValidatorsState(stateSlot); + const readStream = await this.clClient.getValidatorsState(stateSlot); this.logger.log('Processing all validators state'); - const setSummary = (): void => { - for (const state of states) { + let activeValidatorsCount = 0; + let activeValidatorsEffectiveBalance = 0n; + const pipeline = chain([ + readStream, + parser(), + pick({ filter: 'data' }), + streamArray(), + (data) => { + const state: StateValidatorResponse = data.value; const index = BigInt(state.index); const operator = keysIndexed.get(state.validator.pubkey); this.summary.set(index, { @@ -40,9 +51,24 @@ export class StateService { val_slashed: state.validator.slashed, val_status: state.status, val_balance: BigInt(state.balance), + val_effective_balance: BigInt(state.validator.effective_balance), }); - } - }; - await Promise.all([this.storage.writeIndexes(states), setSummary()]); + if ([ValStatus.ActiveOngoing, ValStatus.ActiveExiting, ValStatus.ActiveSlashed].includes(state.status)) { + activeValidatorsCount++; + activeValidatorsEffectiveBalance += BigInt(state.validator.effective_balance); + } + return { val_id: state.index, val_pubkey: state.validator.pubkey }; + }, + ]); + await this.storage.writeIndexes(pipeline); + // todo: change to bigint.sqrt + const baseReward = Math.trunc((64 * 10 ** 9) / Math.trunc(Math.sqrt(Number(activeValidatorsEffectiveBalance)))); + this.summary.setMeta({ + state: { + active_validators: activeValidatorsCount, + active_validators_total_increments: activeValidatorsEffectiveBalance / BigInt(10 ** 9), + base_reward: baseReward, + }, + }); } } diff --git a/src/duty/summary/summary.metrics.ts b/src/duty/summary/summary.metrics.ts index 2db009a8..1370e5ed 100644 --- a/src/duty/summary/summary.metrics.ts +++ b/src/duty/summary/summary.metrics.ts @@ -3,19 +3,103 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { ConfigService } from 'common/config'; import { ConsensusProviderService } from 'common/eth-providers'; -import { PrometheusService } from 'common/prometheus'; +import { PrometheusService, TrackTask } from 'common/prometheus'; +import { RegistryService, RegistrySourceOperator } from 'common/validators-registry'; +import { ClickhouseService } from 'storage'; + +enum Duty { + Proposal = 'proposal', + Sync = 'sync', + Attestation = 'attestation', +} @Injectable() export class SummaryMetrics { + protected processedEpoch: bigint; + protected operators: RegistrySourceOperator[]; public constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, protected readonly config: ConfigService, protected readonly prometheus: PrometheusService, protected readonly clClient: ConsensusProviderService, + protected readonly registryService: RegistryService, + protected readonly storage: ClickhouseService, ) {} + @TrackTask('calc-summary-metrics') public async calculate(epoch: bigint) { - this.prometheus.epochTime = await this.clClient.getSlotTime(epoch * 32n); - this.prometheus.epochNumber.set(Number(epoch)); + this.logger.log('Calculating propose metrics'); + this.processedEpoch = epoch; + this.operators = await this.registryService.getOperators(); + await Promise.all([this.userRewards(), this.avgChainRewards(), this.common()]); + } + + private async common() { + this.prometheus.epochTime = await this.clClient.getSlotTime(this.processedEpoch * 32n); + this.prometheus.epochNumber.set(Number(this.processedEpoch)); + } + + private async avgChainRewards() { + const result = await this.storage.getAvgChainRewardsAndPenaltiesStats(this.processedEpoch); + // Rewards + this.prometheus.avgChainReward.set({ duty: Duty.Attestation }, result ? result.att_reward : 0); + this.prometheus.avgChainReward.set({ duty: Duty.Proposal }, result ? result.prop_reward : 0); + this.prometheus.avgChainReward.set({ duty: Duty.Sync }, result ? result.sync_reward : 0); + // Missed rewards + this.prometheus.avgChainMissedReward.set({ duty: Duty.Attestation }, result ? result.att_missed : 0); + this.prometheus.avgChainMissedReward.set({ duty: Duty.Proposal }, result ? result.prop_missed : 0); + this.prometheus.avgChainMissedReward.set({ duty: Duty.Sync }, result ? result.sync_missed : 0); + // Penalty + this.prometheus.avgChainPenalty.set({ duty: Duty.Attestation }, result ? result.att_penalty : 0); + this.prometheus.avgChainPenalty.set({ duty: Duty.Proposal }, result ? result.prop_penalty : 0); + this.prometheus.avgChainPenalty.set({ duty: Duty.Sync }, result ? result.sync_penalty : 0); + } + + private async userRewards() { + const result = await this.storage.getUserNodeOperatorsRewardsAndPenaltiesStats(this.processedEpoch); + this.operators.forEach((operator) => { + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); + // Rewards + this.prometheus.operatorReward.set( + { nos_name: operator.name, duty: Duty.Attestation }, + operatorResult ? operatorResult.att_reward : 0, + ); + this.prometheus.operatorReward.set({ nos_name: operator.name, duty: Duty.Proposal }, operatorResult ? operatorResult.prop_reward : 0); + this.prometheus.operatorReward.set({ nos_name: operator.name, duty: Duty.Sync }, operatorResult ? operatorResult.sync_reward : 0); + // Missed rewards + this.prometheus.operatorMissedReward.set( + { nos_name: operator.name, duty: Duty.Attestation }, + operatorResult ? operatorResult.att_missed : 0, + ); + this.prometheus.operatorMissedReward.set( + { nos_name: operator.name, duty: Duty.Proposal }, + operatorResult ? operatorResult.prop_missed : 0, + ); + this.prometheus.operatorMissedReward.set( + { nos_name: operator.name, duty: Duty.Sync }, + operatorResult ? operatorResult.sync_missed : 0, + ); + // Penalty + this.prometheus.operatorPenalty.set( + { nos_name: operator.name, duty: Duty.Attestation }, + operatorResult ? operatorResult.att_penalty : 0, + ); + this.prometheus.operatorPenalty.set( + { nos_name: operator.name, duty: Duty.Proposal }, + operatorResult ? operatorResult.prop_penalty : 0, + ); + this.prometheus.operatorPenalty.set({ nos_name: operator.name, duty: Duty.Sync }, operatorResult ? operatorResult.sync_penalty : 0); + // Balance deltas (calculated and real) + this.prometheus.operatorRealBalanceDelta.set({ nos_name: operator.name }, operatorResult ? operatorResult.real_balance_change : 0); + this.prometheus.operatorCalculatedBalanceDelta.set( + { nos_name: operator.name }, + operatorResult ? operatorResult.calculated_balance_change : 0, + ); + // Calculation error + this.prometheus.operatorCalculatedBalanceCalculationError.set( + { nos_name: operator.name }, + operatorResult ? operatorResult.calculation_error : 0, + ); + }); } } diff --git a/src/duty/summary/summary.module.ts b/src/duty/summary/summary.module.ts index b4f94742..56c208c2 100644 --- a/src/duty/summary/summary.module.ts +++ b/src/duty/summary/summary.module.ts @@ -1,12 +1,14 @@ import { Module } from '@nestjs/common'; import { ConsensusProviderModule } from 'common/eth-providers'; +import { RegistryModule } from 'common/validators-registry'; +import { ClickhouseModule } from 'storage/clickhouse'; import { SummaryMetrics } from './summary.metrics'; import { SummaryService } from './summary.service'; @Module({ - imports: [ConsensusProviderModule], + imports: [RegistryModule, ConsensusProviderModule, ClickhouseModule], providers: [SummaryService, SummaryMetrics], exports: [SummaryService, SummaryMetrics], }) diff --git a/src/duty/summary/summary.service.ts b/src/duty/summary/summary.service.ts index c180d65a..61bcc49c 100644 --- a/src/duty/summary/summary.service.ts +++ b/src/duty/summary/summary.service.ts @@ -1,7 +1,19 @@ import { Injectable } from '@nestjs/common'; +import { merge } from 'lodash'; import { ValStatus } from 'common/eth-providers'; +type BlockNumber = bigint; +type ValidatorId = bigint; + +interface ValidatorAttestationReward { + source: number; + target: number; + head: number; +} + +interface ValidatorAttestationPenalty extends ValidatorAttestationReward {} + export interface ValidatorDutySummary { epoch: bigint; /// @@ -11,6 +23,7 @@ export interface ValidatorDutySummary { val_slashed?: boolean; val_status?: ValStatus; val_balance?: bigint; + val_effective_balance?: bigint; /// is_proposer?: boolean; block_to_propose?: bigint; @@ -24,14 +37,62 @@ export interface ValidatorDutySummary { att_valid_head?: boolean; att_valid_target?: boolean; att_valid_source?: boolean; + // Metadata. Necessary for calculating rewards and will not be stored in DB + sync_meta?: { + synced_blocks?: bigint[]; + }; + att_meta?: { + included_in_block?: bigint; + reward_per_increment?: ValidatorAttestationReward; + penalty_per_increment?: ValidatorAttestationPenalty; + }; + // Rewards + att_earned_reward?: bigint; + att_missed_reward?: bigint; + att_penalty?: bigint; + sync_earned_reward?: bigint; + sync_missed_reward?: bigint; + sync_penalty?: bigint; + propose_earned_reward?: bigint; + propose_missed_reward?: bigint; + propose_penalty?: bigint; +} + +export interface EpochMeta { + // will be stored in DB in separate table + state?: { + active_validators?: number; + active_validators_total_increments?: bigint; + base_reward?: number; + }; + attestation?: { + participation?: { source: bigint; target: bigint; head: bigint }; + blocks_rewards?: Map; + }; + sync?: { + blocks_rewards?: Map; + per_block_reward?: bigint; + blocks_to_sync?: bigint[]; + }; } @Injectable() export class SummaryService { - protected storage: Map; + protected storage: Map; + protected meta: EpochMeta; constructor() { - this.storage = new Map(); + this.storage = new Map(); + this.meta = {}; + } + + public setMeta(val: EpochMeta) { + const curr = this.meta ?? {}; + this.meta = merge(curr, val); + } + + public getMeta() { + return this.meta; } public get(index: bigint) { @@ -39,14 +100,23 @@ export class SummaryService { } public set(index: bigint, summary: ValidatorDutySummary) { - this.storage.set(index, { ...(this.get(index) ?? {}), ...summary }); + const curr = this.get(index) ?? {}; + this.storage.set(index, merge(curr, summary)); } - public values(): ValidatorDutySummary[] { - return [...this.storage.values()]; + public values(): IterableIterator { + return this.storage.values(); + } + + public valuesToWrite(): ValidatorDutySummary[] { + return [...this.storage.values()].map((v) => ({ ...v, att_meta: undefined, sync_meta: undefined })); } public clear() { this.storage.clear(); } + + public clearMeta() { + delete this.meta; + } } diff --git a/src/duty/sync/index.ts b/src/duty/sync/index.ts index 81e7b8af..bd4050dd 100644 --- a/src/duty/sync/index.ts +++ b/src/duty/sync/index.ts @@ -1,3 +1,4 @@ export * from './sync.module'; export * from './sync.service'; export * from './sync.metrics'; +export * from './sync.rewards'; diff --git a/src/duty/sync/sync.constants.ts b/src/duty/sync/sync.constants.ts new file mode 100644 index 00000000..66aaad3f --- /dev/null +++ b/src/duty/sync/sync.constants.ts @@ -0,0 +1,13 @@ +// from https://eth2book.info/bellatrix/part2/incentives/rewards/ +const SYNC_REWARD_WEIGHT = 2; +const WEIGHT_DENOMINATOR = 64; + +export const SYNC_COMMITTEE_SIZE = 512; + +export const syncReward = ( + t: bigint, // Total validators increments (effective eths) + b: number, // Base reward per increment +) => { + // per synced slot + return (BigInt(SYNC_REWARD_WEIGHT) * t * BigInt(b)) / BigInt(32 * SYNC_COMMITTEE_SIZE * WEIGHT_DENOMINATOR); +}; diff --git a/src/duty/sync/sync.metrics.ts b/src/duty/sync/sync.metrics.ts index 2223556c..adf6d9dd 100644 --- a/src/duty/sync/sync.metrics.ts +++ b/src/duty/sync/sync.metrics.ts @@ -47,8 +47,9 @@ export class SyncMetrics { private async operatorAvgSyncPercents() { const result = await this.storage.getOperatorSyncParticipationAvgPercents(this.processedEpoch); - result.forEach((p) => { - this.prometheus.operatorSyncParticipationAvgPercent.set({ nos_name: p.val_nos_name }, p.avg_percent); + this.operators.forEach((operator) => { + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); + this.prometheus.operatorSyncParticipationAvgPercent.set({ nos_name: operator.name }, operatorResult ? operatorResult.avg_percent : 0); }); } @@ -71,23 +72,23 @@ export class SyncMetrics { private async goodSyncParticipationLastEpoch(chainAvgSyncPercent: number) { const result = await this.storage.getValidatorsCountWithGoodSyncParticipationLastNEpoch(this.processedEpoch, 1, chainAvgSyncPercent); this.operators.forEach((operator) => { - const operatorResult = result.find((p) => p.val_nos_name == operator.name); + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); this.prometheus.validatorsCountWithGoodSyncParticipation.set({ nos_name: operator.name }, operatorResult ? operatorResult.amount : 0); }); - const other = result.find((p) => p.val_nos_name == 'NULL'); + const other = result.find((p) => p.val_nos_id == null); this.prometheus.otherValidatorsCountWithGoodSyncParticipation.set(other ? other.amount : 0); } private async badSyncParticipationLastEpoch(chainAvgSyncPercent: number) { const result = await this.storage.getValidatorsCountWithBadSyncParticipationLastNEpoch(this.processedEpoch, 1, chainAvgSyncPercent); this.operators.forEach((operator) => { - const operatorResult = result.find((p) => p.val_nos_name == operator.name); + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); this.prometheus.validatorsCountWithSyncParticipationLessAvg.set( { nos_name: operator.name }, operatorResult ? operatorResult.amount : 0, ); }); - const other = result.find((p) => p.val_nos_name == 'NULL'); + const other = result.find((p) => p.val_nos_id == null); this.prometheus.otherValidatorsCountWithSyncParticipationLessAvg.set(other ? other.amount : 0); } @@ -98,7 +99,7 @@ export class SyncMetrics { chainAvgSyncPercent, ); this.operators.forEach((operator) => { - const operatorResult = result.find((p) => p.val_nos_name == operator.name); + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); this.prometheus.validatorsCountWithSyncParticipationLessAvgLastNEpoch.set( { nos_name: operator.name, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, @@ -116,7 +117,7 @@ export class SyncMetrics { possibleHighRewardValidators, ); this.operators.forEach((operator) => { - const operatorResult = result.find((p) => p.val_nos_name == operator.name); + const operatorResult = result.find((p) => p.val_nos_id != null && +p.val_nos_id == operator.index); this.prometheus.highRewardValidatorsCountWithSyncParticipationLessAvgLastNEpoch.set( { nos_name: operator.name, epoch_interval: this.epochInterval }, operatorResult ? operatorResult.amount : 0, diff --git a/src/duty/sync/sync.module.ts b/src/duty/sync/sync.module.ts index a9234c6b..c0fcbb6a 100644 --- a/src/duty/sync/sync.module.ts +++ b/src/duty/sync/sync.module.ts @@ -6,11 +6,12 @@ import { ClickhouseModule } from 'storage/clickhouse'; import { SummaryModule } from '../summary'; import { SyncMetrics } from './sync.metrics'; +import { SyncRewards } from './sync.rewards'; import { SyncService } from './sync.service'; @Module({ imports: [RegistryModule, ConsensusProviderModule, ClickhouseModule, SummaryModule], - providers: [SyncService, SyncMetrics], - exports: [SyncService, SyncMetrics], + providers: [SyncService, SyncMetrics, SyncRewards], + exports: [SyncService, SyncMetrics, SyncRewards], }) export class SyncModule {} diff --git a/src/duty/sync/sync.rewards.ts b/src/duty/sync/sync.rewards.ts new file mode 100644 index 00000000..d6e9481e --- /dev/null +++ b/src/duty/sync/sync.rewards.ts @@ -0,0 +1,34 @@ +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; +import { Inject, Injectable, LoggerService } from '@nestjs/common'; + +import { ConfigService } from 'common/config'; +import { PrometheusService } from 'common/prometheus'; + +import { SummaryService } from '../summary'; + +@Injectable() +export class SyncRewards { + public constructor( + @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, + protected readonly config: ConfigService, + protected readonly prometheus: PrometheusService, + protected readonly summary: SummaryService, + ) {} + + public calculate(epoch: bigint) { + const epochMeta = this.summary.getMeta(); + let sync_earned_reward = 0n; + let sync_missed_reward = 0n; + let sync_penalty = 0n; + const perfectSync = epochMeta.sync.per_block_reward * BigInt(epochMeta.sync.blocks_to_sync.length); + for (const v of this.summary.values()) { + if (!v.is_sync) continue; + sync_earned_reward = epochMeta.sync.per_block_reward * BigInt(v.sync_meta.synced_blocks.length); + sync_penalty = epochMeta.sync.per_block_reward * BigInt(epochMeta.sync.blocks_to_sync.length - v.sync_meta.synced_blocks.length); + sync_missed_reward = perfectSync - sync_earned_reward; + + this.summary.set(v.val_id, { epoch, val_id: v.val_id, sync_earned_reward, sync_penalty, sync_missed_reward }); + } + return true; + } +} diff --git a/src/duty/sync/sync.service.ts b/src/duty/sync/sync.service.ts index 29f9deb8..a5439ea8 100644 --- a/src/duty/sync/sync.service.ts +++ b/src/duty/sync/sync.service.ts @@ -8,8 +8,7 @@ import { StateId } from 'common/eth-providers/consensus-provider/types'; import { PrometheusService, TrackTask } from 'common/prometheus'; import { SummaryService } from '../summary'; - -const SYNC_COMMITTEE_SIZE = 512; +import { SYNC_COMMITTEE_SIZE } from './sync.constants'; @Injectable() export class SyncService { @@ -35,23 +34,32 @@ export class SyncService { blockInfo ? epochBlocks.push(blockInfo) : missedSlots.push(slot); } this.logger.debug(`All missed slots in getting sync committee info process: ${missedSlots}`); - const epochBlocksBits = epochBlocks.map((block) => - SyncCommitteeBits.deserialize(fromHexString(block.message.body.sync_aggregate.sync_committee_bits)).toBoolArray(), - ); + const epochBlocksBits = epochBlocks.map((block) => { + return { + block: BigInt(block.message.slot), + bits: SyncCommitteeBits.deserialize(fromHexString(block.message.body.sync_aggregate.sync_committee_bits)), + }; + }); for (const indexedValidator of indexedValidators) { - let sync_count = 0; - for (const bits of epochBlocksBits) { - if (bits[indexedValidator.in_committee_index]) sync_count++; + const synced_blocks: bigint[] = []; + for (const blockBits of epochBlocksBits) { + if (blockBits.bits.get(indexedValidator.in_committee_index)) { + synced_blocks.push(blockBits.block); + } } const index = BigInt(indexedValidator.validator_index); - const percent = (sync_count / epochBlocksBits.length) * 100; + const percent = (synced_blocks.length / epochBlocksBits.length) * 100; this.summary.set(index, { epoch, val_id: index, is_sync: true, sync_percent: percent, + sync_meta: { + synced_blocks, + }, }); } + this.summary.setMeta({ sync: { blocks_to_sync: epochBlocksBits.map((b) => b.block) } }); } public async getSyncCommitteeIndexedValidators(epoch: bigint, stateId: StateId): Promise { diff --git a/src/inspector/inspector.service.ts b/src/inspector/inspector.service.ts index c385ca7e..cc88f163 100644 --- a/src/inspector/inspector.service.ts +++ b/src/inspector/inspector.service.ts @@ -4,17 +4,15 @@ import { Inject, Injectable, LoggerService, OnModuleInit } from '@nestjs/common' import { CriticalAlertsService } from 'common/alertmanager'; import { ConfigService } from 'common/config'; import { BlockHeaderResponse, ConsensusProviderService } from 'common/eth-providers'; +import { BlockCacheService } from 'common/eth-providers/consensus-provider/block-cache'; import { sleep } from 'common/functions/sleep'; -import { PrometheusService } from 'common/prometheus'; +import { PrometheusService, TrackTask } from 'common/prometheus'; import { DutyMetrics, DutyService } from 'duty'; import { ClickhouseService } from 'storage'; - -import { BlockCacheService } from '../common/eth-providers/consensus-provider/block-cache'; +import { EpochProcessingState } from 'storage/clickhouse'; @Injectable() export class InspectorService implements OnModuleInit { - public latestProcessedEpoch = 0n; - public constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, protected readonly config: ConfigService, @@ -30,9 +28,9 @@ export class InspectorService implements OnModuleInit { public async onModuleInit(): Promise { this.logger.log(`Starting epoch [${this.config.get('START_EPOCH')}]`); - this.latestProcessedEpoch = await this.storage.getMaxEpoch(); - this.prometheus.epochTime = await this.clClient.getSlotTime(this.latestProcessedEpoch * 32n); - this.prometheus.epochNumber.set(Number(this.latestProcessedEpoch)); + const latestProcessedEpoch = await this.storage.getLastProcessedEpoch(); + this.prometheus.epochTime = await this.clClient.getSlotTime(latestProcessedEpoch.epoch * 32n); + this.prometheus.epochNumber.set(Number(latestProcessedEpoch.epoch)); } public async startLoop(): Promise { @@ -42,16 +40,17 @@ export class InspectorService implements OnModuleInit { // eslint-disable-next-line no-constant-condition while (true) { try { - const nextFinalized = await this.waitForNextFinalizedSlot(); - if (nextFinalized) { - const { epoch, stateSlot } = nextFinalized; - const [possibleHighRewardValidators] = await Promise.all([ - this.dutyMetrics.getPossibleHighRewardValidators(), - this.dutyService.checkAndWrite(epoch, stateSlot), - ]); - await this.dutyMetrics.calculate(epoch, possibleHighRewardValidators); + const toProcess = await this.getEpochDataToProcess(); + if (toProcess) { + const { epoch, slot, is_stored, is_calculated } = toProcess; + let possibleHighRewardValidators = []; + if (!is_stored) { + possibleHighRewardValidators = await this.dutyService.checkAndWrite(epoch, slot); + } + if (!is_calculated) { + await this.dutyMetrics.calculate(epoch, possibleHighRewardValidators); + } await this.criticalAlerts.send(epoch); - this.latestProcessedEpoch = epoch; } } catch (e) { this.logger.error(`Error while processing and writing epoch`); @@ -69,16 +68,16 @@ export class InspectorService implements OnModuleInit { } } - protected async waitForNextFinalizedSlot(): Promise<{ epoch: bigint; stateSlot: bigint } | undefined> { - const nextSlot = this.calculateNextFinalizedSlot(); + protected async getEpochDataToProcess(): Promise { + const chosen = await this.chooseEpochToProcess(); const latestFinalizedBeaconBlock = await this.clClient.getBlockHeader('finalized'); const latestFinalizedEpoch = BigInt(latestFinalizedBeaconBlock.header.message.slot) / 32n; - if (BigInt(latestFinalizedBeaconBlock.header.message.slot) < nextSlot) { - // new finalized slot hasn't happened, from which we should get information about needed + if (latestFinalizedEpoch < chosen.epoch) { + // new finalized epoch hasn't happened, from which we should get information about needed // just wait `CHAIN_SLOT_TIME_SECONDS` until finality happens const sleepTime = this.config.get('CHAIN_SLOT_TIME_SECONDS'); this.logger.log( - `Latest finalized epoch [${latestFinalizedEpoch}] found. Latest DB epoch [${this.latestProcessedEpoch}]. Waiting [${sleepTime}] seconds for next finalized slot [${nextSlot}]`, + `Latest finalized epoch [${latestFinalizedEpoch}]. Waiting [${sleepTime}] seconds for next finalized epoch [${chosen.epoch}]`, ); return new Promise((resolve) => { @@ -86,38 +85,46 @@ export class InspectorService implements OnModuleInit { }); } // new finalized slot has happened, from which we can get information about needed - this.logger.log( - `Latest finalized epoch [${latestFinalizedEpoch}] found. Next slot [${nextSlot}]. Latest DB epoch [${this.latestProcessedEpoch}]`, - ); - - const nextProcessedEpoch = nextSlot / 32n; - const nextProcessedHeader = await this.clClient.getBeaconBlockHeaderOrPreviousIfMissed(nextSlot); - if (nextSlot == BigInt(nextProcessedHeader.header.message.slot)) { + this.logger.log(`Latest finalized epoch [${latestFinalizedEpoch}]. Next epoch to process [${chosen.epoch}]`); + const existedHeader = (await this.clClient.getBeaconBlockHeaderOrPreviousIfMissed(chosen.slot)).header.message; + if (chosen.slot == BigInt(existedHeader.slot)) { this.logger.log( - `Fetched next epoch [${nextProcessedEpoch}] by slot [${nextSlot}] with state root [${nextProcessedHeader.header.message.state_root}]`, + `Epoch [${chosen.epoch}] is chosen to process with state slot [${chosen.slot}] with root [${existedHeader.state_root}]`, ); } else { this.logger.log( - `Fetched next epoch [${nextProcessedEpoch}] by slot [${nextProcessedHeader.header.message.slot}] with state root [${ - nextProcessedHeader.header.message.state_root - }] instead of slot [${nextSlot}]. Difference [${BigInt(nextProcessedHeader.header.message.slot) - nextSlot}] slots`, + `Epoch [${chosen.epoch}] is chosen to process with state slot [${existedHeader.slot}] with root [${existedHeader.state_root}] ` + + `instead of slot [${chosen.slot}]. Difference [${BigInt(existedHeader.slot) - chosen.slot}] slots`, ); } return { - epoch: nextProcessedEpoch, - stateSlot: nextProcessedHeader.header.message.slot, + ...chosen, + slot: existedHeader.slot, }; } - protected calculateNextFinalizedSlot(): bigint { + @TrackTask('choose-epoch-to-process') + protected async chooseEpochToProcess(): Promise { const step = BigInt(this.config.get('FETCH_INTERVAL_SLOTS')); - let startEpoch = BigInt(this.config.get('START_EPOCH')); - if (this.latestProcessedEpoch >= startEpoch) { - startEpoch = this.latestProcessedEpoch + 1n; + let next: EpochProcessingState = { epoch: BigInt(this.config.get('START_EPOCH')), is_stored: false, is_calculated: false }; + let lastProcessed = await this.storage.getLastProcessedEpoch(); + const last = await this.storage.getLastEpoch(); + if (last.epoch == 0n) { + // if it's first time, we should get max stored in summary table + const max = await this.storage.getMaxEpoch(); + lastProcessed = { epoch: max.max, is_stored: true, is_calculated: true }; + } + this.logger.log(`Last processed epoch [${lastProcessed.epoch}]`); + if ((last.is_stored && !last.is_calculated) || (!last.is_stored && last.is_calculated)) { + this.logger.debug(JSON.stringify(last)); + this.logger.warn(`Epoch [${last.epoch}] processing was not completed correctly. Trying to complete`); + next = last; + } + if (lastProcessed.epoch >= next.epoch) { + next.epoch = lastProcessed.epoch + 1n; } - const slotToProcess = startEpoch * step + (step - 1n); // latest slot in epoch - this.logger.log(`Slot to process [${slotToProcess}] (end of epoch [${startEpoch}])`); - return slotToProcess; + this.logger.log(`Next epoch to process [${next.epoch}]`); + return { ...next, slot: next.epoch * step + (step - 1n) }; } } diff --git a/src/storage/clickhouse/clickhouse.constants.ts b/src/storage/clickhouse/clickhouse.constants.ts index 5672344b..512b4b78 100644 --- a/src/storage/clickhouse/clickhouse.constants.ts +++ b/src/storage/clickhouse/clickhouse.constants.ts @@ -1,21 +1,22 @@ import { ValStatus } from 'common/eth-providers'; -export const validatorBalancesDeltaQuery = (epoch: bigint): string => ` +export const avgValidatorBalanceDelta = (epoch: bigint): string => ` SELECT - val_nos_name, + val_nos_id, avg(current.val_balance - previous.val_balance) AS delta FROM ( - SELECT val_balance, val_id, val_nos_name + SELECT val_balance, val_id, val_nos_id FROM validators_summary WHERE val_status != '${ValStatus.PendingQueued}' AND val_nos_id IS NOT NULL AND epoch = ${epoch} + LIMIT 1 BY val_id ) AS current INNER JOIN ( - SELECT val_balance, val_id, val_nos_name + SELECT val_balance, val_id, val_nos_id FROM validators_summary WHERE val_status != '${ValStatus.PendingQueued}' AND @@ -24,26 +25,26 @@ export const validatorBalancesDeltaQuery = (epoch: bigint): string => ` ) AS previous ON previous.val_id = current.val_id - GROUP BY val_nos_name - ORDER BY delta DESC + GROUP BY val_nos_id `; export const validatorQuantile0001BalanceDeltasQuery = (epoch: bigint): string => ` SELECT - val_nos_name, + val_nos_id, quantileExact(0.001)(current.val_balance - previous.val_balance) AS delta FROM ( - SELECT val_balance, val_id, val_nos_name + SELECT val_balance, val_id, val_nos_id FROM validators_summary WHERE val_status != '${ValStatus.PendingQueued}' AND val_nos_id IS NOT NULL AND epoch = ${epoch} + LIMIT 1 BY val_id ) AS current INNER JOIN ( - SELECT val_balance, val_id, val_nos_name + SELECT val_balance, val_id, val_nos_id FROM validators_summary WHERE val_status != '${ValStatus.PendingQueued}' AND @@ -52,26 +53,26 @@ export const validatorQuantile0001BalanceDeltasQuery = (epoch: bigint): string = ) AS previous ON previous.val_id = current.val_id - GROUP BY val_nos_name - ORDER BY delta DESC + GROUP BY val_nos_id `; export const validatorsCountWithNegativeDeltaQuery = (epoch: bigint): string => ` SELECT - val_nos_name, - COUNT(val_id) AS neg_count + val_nos_id, + count(val_id) AS neg_count FROM ( - SELECT val_balance, val_id, val_nos_name + SELECT val_balance, val_id, val_nos_id FROM validators_summary WHERE val_status != '${ValStatus.PendingQueued}' AND val_nos_id IS NOT NULL AND epoch = ${epoch} + LIMIT 1 BY val_id ) AS current INNER JOIN ( - SELECT val_balance, val_id, val_nos_name + SELECT val_balance, val_id, val_nos_id FROM validators_summary WHERE val_status != '${ValStatus.PendingQueued}' AND @@ -80,9 +81,8 @@ export const validatorsCountWithNegativeDeltaQuery = (epoch: bigint): string => ) AS previous ON previous.val_id = current.val_id - GROUP BY val_nos_name + GROUP BY val_nos_id HAVING (current.val_balance - previous.val_balance) < 0 - ORDER BY neg_count DESC `; export const validatorsCountWithSyncParticipationByConditionLastNEpochQuery = ( @@ -97,29 +97,26 @@ export const validatorsCountWithSyncParticipationByConditionLastNEpochQuery = ( } return ` SELECT - val_nos_name, + val_nos_id, count() as amount FROM ( SELECT - val_nos_name, + val_nos_id, count() AS count_fail FROM ( - SELECT - val_id, - val_nos_name - FROM - validators_summary + SELECT val_id, val_nos_id + FROM validators_summary WHERE is_sync = 1 AND ${condition} AND (epoch <= ${epoch} AND epoch > (${epoch} - ${epochInterval})) ${strFilterValIndexes} + LIMIT 1 BY epoch, val_id ) - GROUP BY val_id, val_nos_name - ORDER BY count_fail DESC, val_id + GROUP BY val_id, val_nos_id ) WHERE count_fail = ${epochInterval} - GROUP BY val_nos_name + GROUP BY val_nos_id `; }; @@ -135,52 +132,48 @@ export const validatorCountByConditionAttestationLastNEpochQuery = ( } return ` SELECT - val_nos_name, + val_nos_id, count() as amount FROM ( SELECT - val_id, - count() as count_fail, - val_nos_name + val_nos_id, + count() as count_fail FROM ( - SELECT - val_id, - val_nos_name + SELECT val_id, val_nos_id FROM validators_summary WHERE ${condition} AND (epoch <= ${epoch} AND epoch > (${epoch} - ${epochInterval})) ${strFilterValIndexes} - ORDER BY epoch DESC, val_id + LIMIT 1 BY epoch, val_id ) - GROUP BY val_id, val_nos_name - ORDER BY count_fail DESC, val_id + GROUP BY val_id, val_nos_id ) WHERE count_fail = ${epochInterval} - GROUP BY val_nos_name - ORDER BY amount DESC + GROUP BY val_nos_id `; }; export const validatorCountHighAvgIncDelayAttestationOfNEpochQuery = (epoch: bigint, epochInterval: number): string => { return ` SELECT - val_nos_name, + val_nos_id, count() as amount FROM ( SELECT - val_id, - avg(att_inc_delay) as avg_inclusion_delay, - val_nos_name - FROM validators_summary - WHERE - (epoch <= ${epoch} AND epoch > (${epoch} - ${epochInterval})) - GROUP BY val_id, val_nos_name + val_nos_id, + avg(att_inc_delay) as avg_inclusion_delay + FROM ( + SELECT val_id, val_nos_id, att_inc_delay + FROM validators_summary + WHERE + (epoch <= ${epoch} AND epoch > (${epoch} - ${epochInterval})) + LIMIT 1 BY epoch, val_id + ) + GROUP BY val_id, val_nos_id HAVING avg_inclusion_delay > 2 - ORDER BY avg_inclusion_delay DESC, val_id ) - GROUP BY val_nos_name - ORDER BY amount DESC + GROUP BY val_nos_id `; }; @@ -191,80 +184,95 @@ export const validatorsCountByConditionMissProposeQuery = (epoch: bigint, valida } return ` SELECT - val_nos_name, + val_nos_id, count() as amount FROM ( - SELECT - val_id, - val_nos_name + SELECT val_nos_id FROM validators_summary WHERE is_proposer = 1 AND ${condition} AND (epoch <= ${epoch} AND epoch > (${epoch} - 1)) ${strFilterValIndexes} - ORDER BY epoch DESC, val_id + LIMIT 1 BY epoch, val_id ) - GROUP BY val_nos_name - ORDER BY amount DESC + GROUP BY val_nos_id `; }; export const userSyncParticipationAvgPercentQuery = (epoch: bigint): string => ` - SELECT - avg(sync_percent) as avg_percent - FROM - validators_summary - WHERE is_sync = 1 AND val_nos_id IS NOT NULL AND epoch = ${epoch} + SELECT + avg(sync_percent) as avg_percent + FROM ( + SELECT sync_percent + FROM validators_summary + WHERE + is_sync = 1 AND val_nos_id IS NOT NULL AND epoch = ${epoch} + LIMIT 1 BY val_id + ) `; export const otherSyncParticipationAvgPercentQuery = (epoch: bigint): string => ` - SELECT - avg(sync_percent) as avg_percent - FROM - validators_summary - WHERE is_sync = 1 AND val_nos_id IS NULL AND epoch = ${epoch} + SELECT + avg(sync_percent) as avg_percent + FROM ( + SELECT sync_percent + FROM validators_summary + WHERE + is_sync = 1 AND val_nos_id IS NULL AND epoch = ${epoch} + LIMIT 1 BY val_id + ) `; export const chainSyncParticipationAvgPercentQuery = (epoch: bigint): string => ` - SELECT - avg(sync_percent) as avg_percent - FROM - validators_summary - WHERE is_sync = 1 AND epoch = ${epoch} + SELECT + avg(sync_percent) as avg_percent + FROM ( + SELECT sync_percent + FROM validators_summary + WHERE + is_sync = 1 AND epoch = ${epoch} + LIMIT 1 BY val_id + ) `; export const operatorsSyncParticipationAvgPercentsQuery = (epoch: bigint): string => ` - SELECT - val_nos_name, - avg(sync_percent) as avg_percent - FROM - validators_summary - WHERE is_sync = 1 AND val_nos_id IS NOT NULL AND epoch = ${epoch} - GROUP BY val_nos_name + SELECT + val_nos_id, + avg(sync_percent) as avg_percent + FROM ( + SELECT val_nos_id, sync_percent + FROM validators_summary + WHERE + is_sync = 1 AND val_nos_id IS NOT NULL AND epoch = ${epoch} + LIMIT 1 BY val_id + ) + GROUP BY val_nos_id `; export const totalBalance24hDifferenceQuery = (epoch: bigint): string => ` SELECT ( SELECT SUM(curr.val_balance) - FROM - validators_summary AS curr - INNER JOIN - ( - SELECT val_balance, val_id, val_nos_id - FROM validators_summary - WHERE - val_status != '${ValStatus.PendingQueued}' AND - val_nos_id IS NOT NULL AND - epoch = ${epoch} - 225 + FROM ( + SELECT val_balance, val_id, val_nos_id + FROM validators_summary + WHERE + epoch = ${epoch} + AND val_status != '${ValStatus.PendingQueued}' + AND val_nos_id IS NOT NULL + LIMIT 1 BY val_id + ) AS curr + INNER JOIN ( + SELECT val_balance, val_id, val_nos_id + FROM validators_summary + WHERE + val_status != '${ValStatus.PendingQueued}' AND + val_nos_id IS NOT NULL AND + epoch = ${epoch} - 225 ) AS previous ON previous.val_nos_id = curr.val_nos_id AND previous.val_id = curr.val_id - WHERE - curr.epoch = ${epoch} - AND curr.val_status != '${ValStatus.PendingQueued}' - AND curr.val_nos_id IS NOT NULL ) as curr_total_balance, ( SELECT SUM(prev.val_balance) @@ -278,48 +286,54 @@ export const totalBalance24hDifferenceQuery = (epoch: bigint): string => ` `; export const operatorBalance24hDifferenceQuery = (epoch: bigint): string => ` - SELECT - val_nos_name, - SUM(curr.val_balance - previous.val_balance) as diff - FROM - validators_summary AS curr - INNER JOIN - ( - SELECT val_balance, val_id, val_nos_id - FROM validators_summary - WHERE - val_status != '${ValStatus.PendingQueued}' AND - val_nos_id IS NOT NULL AND - epoch = ${epoch} - 225 - ) AS previous - ON - previous.val_nos_id = curr.val_nos_id AND - previous.val_id = curr.val_id + SELECT + curr.val_nos_id as val_nos_id, + SUM(curr.val_balance - previous.val_balance) as diff + FROM ( + SELECT val_balance, val_id, val_nos_id + FROM validators_summary + WHERE + epoch = ${epoch} + AND val_status != '${ValStatus.PendingQueued}' + AND val_nos_id IS NOT NULL + LIMIT 1 BY val_id + ) as curr + INNER JOIN ( + SELECT val_balance, val_id, val_nos_id + FROM validators_summary WHERE - curr.epoch = ${epoch} - AND curr.val_status != '${ValStatus.PendingQueued}' - AND curr.val_nos_id IS NOT NULL - GROUP BY curr.val_nos_name + val_status != '${ValStatus.PendingQueued}' AND + val_nos_id IS NOT NULL AND + epoch = ${epoch} - 225 + ) AS previous + ON + previous.val_nos_id = curr.val_nos_id AND + previous.val_id = curr.val_id + GROUP BY curr.val_nos_id `; export const userNodeOperatorsStatsQuery = (epoch: bigint): string => ` SELECT - val_nos_name, + val_nos_id, SUM(a) as active_ongoing, SUM(p) as pending, SUM(s) as slashed - FROM - ( + FROM ( SELECT - val_nos_name, + val_nos_id, IF(val_status = '${ValStatus.ActiveOngoing}', count(val_status), 0) as a, IF(val_status = '${ValStatus.PendingQueued}' OR val_status = '${ValStatus.PendingInitialized}', count(val_status), 0) as p, IF(val_status = '${ValStatus.ActiveSlashed}' OR val_status = '${ValStatus.ExitedSlashed}' OR val_slashed = 1, count(val_status), 0) as s - FROM validators_summary - WHERE val_nos_id IS NOT NULL and epoch = ${epoch} - GROUP BY val_nos_name, val_status, val_slashed + FROM ( + SELECT val_nos_id, val_status, val_slashed + FROM validators_summary + WHERE + val_nos_id IS NOT NULL and epoch = ${epoch} + LIMIT 1 BY val_id + ) + GROUP BY val_nos_id, val_status, val_slashed ) - GROUP by val_nos_name + GROUP by val_nos_id `; export const userValidatorsSummaryStatsQuery = (epoch: bigint): string => ` @@ -333,8 +347,13 @@ export const userValidatorsSummaryStatsQuery = (epoch: bigint): string => ` IF(val_status = '${ValStatus.ActiveOngoing}', count(val_status), 0) as a, IF(val_status = '${ValStatus.PendingQueued}' OR val_status = '${ValStatus.PendingInitialized}', count(val_status), 0) as p, IF(val_status = '${ValStatus.ActiveSlashed}' OR val_status = '${ValStatus.ExitedSlashed}' OR val_slashed = 1, count(val_status), 0) as s - FROM validators_summary - WHERE val_nos_id IS NOT NULL and epoch = ${epoch} + FROM ( + SELECT val_status, val_slashed + FROM validators_summary + WHERE + val_nos_id IS NOT NULL and epoch = ${epoch} + LIMIT 1 BY val_id + ) GROUP BY val_status, val_slashed ) `; @@ -350,26 +369,191 @@ export const otherValidatorsSummaryStatsQuery = (epoch: bigint): string => ` IF(val_status = '${ValStatus.ActiveOngoing}', count(val_status), 0) as a, IF(val_status = '${ValStatus.PendingQueued}' OR val_status = '${ValStatus.PendingInitialized}', count(val_status), 0) as p, IF(val_status = '${ValStatus.ActiveSlashed}' OR val_status = '${ValStatus.ExitedSlashed}' OR val_slashed = 1, count(val_status), 0) as s - FROM validators_summary - WHERE val_nos_id IS NULL and epoch = ${epoch} + FROM ( + SELECT val_status, val_slashed + FROM validators_summary + WHERE + val_nos_id IS NULL and epoch = ${epoch} + LIMIT 1 BY val_id + ) GROUP BY val_status, val_slashed ) `; -export const userNodeOperatorsProposesStatsLastNEpochQuery = (fetchInterval: number, epoch: bigint, epochInterval = 120): string => ` +export const userNodeOperatorsProposesStatsLastNEpochQuery = (epoch: bigint, epochInterval = 120): string => ` SELECT - val_nos_name, + val_nos_id, SUM(a) as all, SUM(m) as missed FROM ( SELECT - val_nos_name, + val_nos_id, count(block_proposed) as a, IF(block_proposed = 0, count(block_proposed), 0) as m - FROM validators_summary - WHERE is_proposer = 1 AND (epoch <= ${epoch} AND epoch > (${epoch} - ${fetchInterval} * ${epochInterval})) - GROUP BY val_nos_name, block_proposed + FROM ( + SELECT val_nos_id, block_proposed + FROM validators_summary + WHERE + is_proposer = 1 AND (epoch <= ${epoch} AND epoch > (${epoch} - ${epochInterval})) + LIMIT 1 BY epoch, val_id + ) + GROUP BY val_nos_id, block_proposed ) - GROUP by val_nos_name + GROUP by val_nos_id +`; + +export const epochMetadata = (epoch: bigint): string => ` + SELECT * + FROM epochs_metadata + WHERE epoch = ${epoch} +`; + +export const epochProcessing = (epoch: bigint): string => ` + SELECT * + FROM epochs_processing + WHERE epoch = ${epoch} +`; + +export const userNodeOperatorsRewardsAndPenaltiesStats = (epoch: bigint): string => ` + SELECT + att.val_nos_id as val_nos_id, + -- + attestation_reward as att_reward, + ifNull(prop_reward, 0) as prop_reward, + ifNull(sync_reward, 0) as sync_reward, + attestation_missed as att_missed, + ifNull(prop_missed, 0) as prop_missed, + ifNull(sync_missed, 0) as sync_missed, + attestation_penalty as att_penalty, + ifNull(prop_penalty, 0) as prop_penalty, + ifNull(sync_penalty, 0) as sync_penalty, + -- + att_reward + prop_reward + sync_reward as total_reward, + att_missed + prop_missed + sync_missed as total_missed, + att_penalty + prop_penalty + sync_penalty as total_penalty, + total_reward - total_penalty as calculated_balance_change, + real_balance_change, + calculated_balance_change - real_balance_change as calculation_error + FROM + ( + SELECT + val_nos_id, + sum(att_earned_reward) as attestation_reward, + sum(att_missed_reward) as attestation_missed, + sum(att_penalty) as attestation_penalty + FROM ( + SELECT val_nos_id, att_earned_reward, att_missed_reward, att_penalty + FROM validators_summary + WHERE val_nos_id IS NOT NULL and epoch = ${epoch} - 2 + LIMIT 1 BY val_id + ) + GROUP BY val_nos_id + ) as att + LEFT JOIN + ( + SELECT + val_nos_id, + sum(propose_earned_reward) as prop_reward, + sum(propose_missed_reward) as prop_missed, + sum(propose_penalty) as prop_penalty + FROM ( + SELECT val_nos_id, propose_earned_reward, propose_missed_reward, propose_penalty + FROM validators_summary + WHERE val_nos_id IS NOT NULL and epoch = ${epoch} and is_proposer = 1 + LIMIT 1 BY val_id + ) + GROUP BY val_nos_id + ) as prop ON att.val_nos_id = prop.val_nos_id + LEFT JOIN + ( + SELECT + val_nos_id, + sum(sync_earned_reward) as sync_reward, + sum(sync_missed_reward) as sync_missed, + sum(sync_penalty) as sync_penalty + FROM ( + SELECT val_nos_id, sync_earned_reward, sync_missed_reward, sync_penalty + FROM validators_summary + WHERE val_nos_id IS NOT NULL and epoch = ${epoch} and is_sync = 1 + LIMIT 1 BY val_id + ) + GROUP BY val_nos_id + ) as sync ON att.val_nos_id = sync.val_nos_id + LEFT JOIN + ( + SELECT + val_nos_id, + sum(current.val_balance - previous.val_balance) AS real_balance_change + FROM ( + SELECT val_balance, val_id, val_nos_id + FROM validators_summary as curr + WHERE + val_status != '${ValStatus.PendingQueued}' AND + val_nos_id IS NOT NULL AND + epoch = ${epoch} + LIMIT 1 BY val_id + ) AS current + INNER JOIN + ( + SELECT val_balance, val_id, val_nos_id + FROM validators_summary + WHERE + val_status != '${ValStatus.PendingQueued}' AND + val_nos_id IS NOT NULL AND + epoch = (${epoch} - 1) + LIMIT 1 BY val_id + ) AS previous ON previous.val_id = current.val_id + GROUP BY val_nos_id + ) as bal ON att.val_nos_id = bal.val_nos_id +`; + +export const avgChainRewardsAndPenaltiesStats = (epoch: bigint): string => ` + SELECT + attestation_reward as att_reward, + ifNull(prop_reward, 0) as prop_reward, + ifNull(sync_reward, 0) as sync_reward, + attestation_missed as att_missed, + ifNull(prop_missed, 0) as prop_missed, + ifNull(sync_missed, 0) as sync_missed, + attestation_penalty as att_penalty, + ifNull(prop_penalty, 0) as prop_penalty, + ifNull(sync_penalty, 0) as sync_penalty + FROM + ( + SELECT + avg(att_earned_reward) as attestation_reward, + avg(att_missed_reward) as attestation_missed, + avg(att_penalty) as attestation_penalty + FROM ( + SELECT att_earned_reward, att_missed_reward, att_penalty + FROM validators_summary + WHERE epoch = ${epoch} - 2 + LIMIT 1 BY val_id + ) + ) as att, + ( + SELECT + avg(propose_earned_reward) as prop_reward, + avg(propose_missed_reward) as prop_missed, + avg(propose_penalty) as prop_penalty + FROM ( + SELECT propose_earned_reward, propose_missed_reward, propose_penalty + FROM validators_summary + WHERE epoch = ${epoch} and is_proposer = 1 + LIMIT 1 BY val_id + ) + ) as prop, + ( + SELECT + avg(sync_earned_reward) as sync_reward, + avg(sync_missed_reward) as sync_missed, + avg(sync_penalty) as sync_penalty + FROM ( + SELECT sync_earned_reward, sync_missed_reward, sync_penalty + FROM validators_summary + WHERE epoch = ${epoch} and is_sync = 1 + LIMIT 1 BY val_id + ) + ) as sync `; diff --git a/src/storage/clickhouse/clickhouse.service.ts b/src/storage/clickhouse/clickhouse.service.ts index 85ba3dff..21e7c470 100644 --- a/src/storage/clickhouse/clickhouse.service.ts +++ b/src/storage/clickhouse/clickhouse.service.ts @@ -1,25 +1,30 @@ +import { Duplex } from 'stream'; + +import { ClickHouseClient, createClient } from '@clickhouse/client'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { Inject, Injectable, LoggerService, OnModuleInit } from '@nestjs/common'; -import { ClickHouse } from 'clickhouse'; import { ConfigService } from 'common/config'; -import { StateValidatorResponse } from 'common/eth-providers'; import { retrier } from 'common/functions/retrier'; import { PrometheusService, TrackTask } from 'common/prometheus'; -import { ValidatorDutySummary } from 'duty/summary'; +import { EpochMeta, ValidatorDutySummary } from 'duty/summary'; import { + avgChainRewardsAndPenaltiesStats, + avgValidatorBalanceDelta, chainSyncParticipationAvgPercentQuery, + epochMetadata, + epochProcessing, operatorBalance24hDifferenceQuery, operatorsSyncParticipationAvgPercentsQuery, otherSyncParticipationAvgPercentQuery, otherValidatorsSummaryStatsQuery, totalBalance24hDifferenceQuery, userNodeOperatorsProposesStatsLastNEpochQuery, + userNodeOperatorsRewardsAndPenaltiesStats, userNodeOperatorsStatsQuery, userSyncParticipationAvgPercentQuery, userValidatorsSummaryStatsQuery, - validatorBalancesDeltaQuery, validatorCountByConditionAttestationLastNEpochQuery, validatorCountHighAvgIncDelayAttestationOfNEpochQuery, validatorQuantile0001BalanceDeltasQuery, @@ -28,11 +33,14 @@ import { validatorsCountWithSyncParticipationByConditionLastNEpochQuery, } from './clickhouse.constants'; import { + AvgChainRewardsStats, + EpochProcessingState, NOsDelta, NOsProposesStats, NOsValidatorsByConditionAttestationCount, NOsValidatorsByConditionProposeCount, NOsValidatorsNegDeltaCount, + NOsValidatorsRewardsStats, NOsValidatorsStatusStats, NOsValidatorsSyncAvgPercent, NOsValidatorsSyncByConditionCount, @@ -41,10 +49,13 @@ import { } from './clickhouse.types'; import migration_000000_summary from './migrations/migration_000000_summary'; import migration_000001_indexes from './migrations/migration_000001_indexes'; +import migration_000002_rewards from './migrations/migration_000002_rewards'; +import migration_000003_epoch_meta from './migrations/migration_000003_epoch_meta'; +import migration_000004_epoch_processing from './migrations/migration_000004_epoch_processing'; @Injectable() export class ClickhouseService implements OnModuleInit { - private readonly db: ClickHouse; + private readonly db: ClickHouseClient; private readonly maxRetries: number; private readonly minBackoff: number; private readonly maxBackoff: number; @@ -66,148 +77,210 @@ export class ClickhouseService implements OnModuleInit { this.retry = retrier(this.logger, this.maxRetries, this.minBackoff * 1000, this.maxBackoff * 1000, true); - this.db = new ClickHouse({ - url: this.config.get('DB_HOST'), - port: parseInt(this.config.get('DB_PORT'), 10), - config: { - database: this.config.get('DB_NAME'), - }, - basicAuth: { - username: this.config.get('DB_USER'), - password: this.config.get('DB_PASSWORD'), + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json + BigInt.prototype['toJSON'] = function () { + return this.toString(); + }; + + this.db = createClient({ + host: `${this.config.get('DB_HOST')}:${this.config.get('DB_PORT')}`, + username: this.config.get('DB_USER'), + password: this.config.get('DB_PASSWORD'), + database: this.config.get('DB_NAME'), + compression: { + response: true, + request: true, }, - isSessionPerQuery: true, }); } + private async select(query: string): Promise { + return await (await this.retry(async () => await this.db.query({ query, format: 'JSONEachRow' }))).json(); + } + public async onModuleInit(): Promise { await this.retry(async () => await this.migrate()); } - public async getMaxEpoch(): Promise { - const data: any = await this.retry(async () => await this.db.query('SELECT max(epoch) as max FROM validators_summary').toPromise()); - const epoch = BigInt(parseInt(data[0].max, 10) || 0); + public async getLastProcessedEpoch(): Promise { + const data = ( + await this.select( + 'SELECT epoch FROM epochs_processing WHERE is_stored = 1 AND is_calculated = 1 ORDER BY epoch DESC LIMIT 1', + ) + )[0]; + if (data) return { ...data, epoch: BigInt(data.epoch) }; + return { epoch: 0n, is_stored: undefined, is_calculated: undefined }; + } - this.logger.log(`Max (latest) stored epoch in DB [${epoch}]`); + public async getLastEpoch(): Promise { + const data = (await this.select('SELECT * FROM epochs_processing ORDER BY epoch DESC LIMIT 1'))[0]; + if (data) return { ...data, epoch: BigInt(data.epoch) }; + return { epoch: 0n, is_stored: undefined, is_calculated: undefined }; + } - return epoch; + public async getMaxEpoch(): Promise<{ max }> { + const data = (await this.select<{ max }[]>('SELECT max(epoch) as max FROM validators_summary'))[0]; + if (data) return { max: BigInt(data.max) }; + return { max: 0n }; } @TrackTask('write-indexes') - public async writeIndexes(states: StateValidatorResponse[]): Promise { - const statesCopy = [...states]; - while (statesCopy.length > 0) { - const chunk = statesCopy.splice(0, this.chunkSize); - const ws = this.db.insert('INSERT INTO validators_index ' + '(val_id, val_pubkey) VALUES').stream(); - for (const v of chunk) { - await ws.writeRow(`(${v.index}, '${v.validator.pubkey}')`); - } - await this.retry(async () => await ws.exec().finally(() => ws.destroy())); - } + public async writeIndexes(pipeline: Duplex): Promise { + await this.db + .insert({ + table: 'validators_index', + values: pipeline, + format: 'JSONEachRow', + }) + .finally(() => pipeline.destroy()); } @TrackTask('write-summary') public async writeSummary(summary: ValidatorDutySummary[]): Promise { while (summary.length > 0) { const chunk = summary.splice(0, this.chunkSize); - const ws = this.db - .insert( - 'INSERT INTO validators_summary ' + - '(epoch, val_id, val_nos_id, val_nos_name, ' + - 'val_slashed, val_status, val_balance, is_proposer, block_to_propose, block_proposed, ' + - 'is_sync, sync_percent, ' + - 'att_happened, att_inc_delay, att_valid_head, att_valid_target, att_valid_source) VALUES', - ) - .stream(); - for (const v of chunk) { - await ws.writeRow( - `(${v.epoch}, ${v.val_id}, ` + - `${v.val_nos_id ?? 'NULL'}, ` + - `'${v.val_nos_name ?? 'NULL'}', ` + - `${v.val_slashed ? 1 : 0}, '${v.val_status}', ${v.val_balance}, ` + - `${v.is_proposer ? 1 : 0}, ${v.block_to_propose ?? 'NULL'}, ${v.is_proposer ? (v.block_proposed ? 1 : 0) : 'NULL'}, ` + - `${v.is_sync ? 1 : 0}, ${v.sync_percent ?? 'NULL'}, ` + - `${v.att_happened != undefined ? (v.att_happened ? 1 : 0) : 'NULL'}, ${v.att_inc_delay ?? 'NULL'}, ` + - `${v.att_valid_head ?? 'NULL'}, ${v.att_valid_target ?? 'NULL'}, ${v.att_valid_source ?? 'NULL'} - )`, - ); - } - await this.retry(async () => await ws.exec().finally(() => ws.destroy())); + await this.retry( + async () => + await this.db.insert({ + table: 'validators_summary', + values: chunk, + format: 'JSONEachRow', + }), + ); } } + @TrackTask('write-epoch-meta') + public async writeEpochMeta(epoch: bigint, meta: EpochMeta): Promise { + await this.retry( + async () => + await this.db.insert({ + table: 'epochs_metadata', + values: [ + { + epoch, + active_validators: meta.state.active_validators, + active_validators_total_increments: meta.state.active_validators_total_increments, + base_reward: meta.state.base_reward, + att_blocks_rewards: Array.from(meta.attestation.blocks_rewards), + att_source_participation: meta.attestation.participation.source, + att_target_participation: meta.attestation.participation.target, + att_head_participation: meta.attestation.participation.head, + sync_blocks_rewards: Array.from(meta.sync.blocks_rewards), + sync_blocks_to_sync: meta.sync.blocks_to_sync, + }, + ], + format: 'JSONEachRow', + }), + ); + } + + @TrackTask('update-epoch-processing') + public async updateEpochProcessing(state: EpochProcessingState): Promise { + const old = await this.getOrInitEpochProcessing(state.epoch); + const updates = []; + if (state.is_stored != undefined) updates.push(`is_stored = ${+state.is_stored}`); + if (state.is_calculated != undefined) updates.push(`is_calculated = ${+state.is_calculated}`); + await this.retry( + async () => await this.db.exec({ query: `ALTER TABLE epochs_processing UPDATE ${updates.join(', ')} WHERE epoch = ${state.epoch}` }), + ); + // update is heavy operation for clickhouse, and it takes some time + await this.retry(async () => { + const updated = await this.getOrInitEpochProcessing(state.epoch); + if (old.is_stored == updated.is_stored && old.is_calculated == updated.is_calculated) { + throw Error('Epoch processing info is not updated yet'); + } + }); + } + public async migrate(): Promise { this.logger.log('Running migrations'); - await this.db.query(migration_000000_summary).toPromise(); - await this.db.query(migration_000001_indexes).toPromise(); + const migrations = [ + migration_000000_summary, + migration_000001_indexes, + migration_000002_rewards, + migration_000003_epoch_meta, + migration_000004_epoch_processing, + ]; + for (const query of migrations) { + await this.db.exec({ query }); + } } - public async getValidatorBalancesDelta(epoch: bigint): Promise { - const ret = await this.retry(async () => await this.db.query(validatorBalancesDeltaQuery(epoch)).toPromise()); - return ret; + public async getAvgValidatorBalanceDelta(epoch: bigint): Promise { + return (await this.select(avgValidatorBalanceDelta(epoch))).map((v) => ({ + ...v, + delta: Number(v.delta), + })); } public async getValidatorQuantile0001BalanceDeltas(epoch: bigint): Promise { - const ret = await this.retry(async () => this.db.query(validatorQuantile0001BalanceDeltasQuery(epoch)).toPromise()); - return ret; + return (await this.select(validatorQuantile0001BalanceDeltasQuery(epoch))).map((v) => ({ + ...v, + delta: Number(v.delta), + })); } public async getValidatorsCountWithNegativeDelta(epoch: bigint): Promise { - const ret = await this.retry(async () => this.db.query(validatorsCountWithNegativeDeltaQuery(epoch)).toPromise()); - return ret; + return (await this.select(validatorsCountWithNegativeDeltaQuery(epoch))).map((v) => ({ + ...v, + neg_count: Number(v.neg_count), + })); } /** * Send query to Clickhouse and receives information about User Sync Committee participants */ public async getUserSyncParticipationAvgPercent(epoch: bigint): Promise { - const ret = await this.retry(async () => this.db.query(userSyncParticipationAvgPercentQuery(epoch)).toPromise()); - return ret[0]; + const ret = await this.select(userSyncParticipationAvgPercentQuery(epoch)); + return { avg_percent: Number(ret[0].avg_percent) }; } /** * Send query to Clickhouse and receives information about Other Sync Committee avg percent */ public async getOtherSyncParticipationAvgPercent(epoch: bigint): Promise { - const ret = await this.retry(async () => this.db.query(otherSyncParticipationAvgPercentQuery(epoch)).toPromise()); - return ret[0]; + const ret = await this.select(otherSyncParticipationAvgPercentQuery(epoch)); + return { avg_percent: Number(ret[0].avg_percent) }; } /** * Send query to Clickhouse and receives information about Chain Sync Committee acg percent */ public async getChainSyncParticipationAvgPercent(epoch: bigint): Promise { - const ret = await this.retry(async () => this.db.query(chainSyncParticipationAvgPercentQuery(epoch)).toPromise()); - return ret[0]; + const ret = await this.select(chainSyncParticipationAvgPercentQuery(epoch)); + return { avg_percent: Number(ret[0].avg_percent) }; } /** * Send query to Clickhouse and receives information about Operator Sync Committee participants */ public async getOperatorSyncParticipationAvgPercents(epoch: bigint): Promise { - const ret = await this.retry(async () => this.db.query(operatorsSyncParticipationAvgPercentsQuery(epoch)).toPromise()); - return ret; + return (await this.select(operatorsSyncParticipationAvgPercentsQuery(epoch))).map((v) => ({ + ...v, + avg_percent: Number(v.avg_percent), + })); } public async getValidatorsCountWithGoodSyncParticipationLastNEpoch( - slot: bigint, + epoch: bigint, epochInterval: number, chainAvg: number, validatorIndexes: string[] = [], ): Promise { - const ret = await this.retry(async () => - this.db - .query( - validatorsCountWithSyncParticipationByConditionLastNEpochQuery( - slot, - epochInterval, - validatorIndexes, - `sync_percent >= (${chainAvg} - ${this.config.get('SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG')})`, - ), - ) - .toPromise(), - ); - return ret; + return ( + await this.select( + validatorsCountWithSyncParticipationByConditionLastNEpochQuery( + epoch, + epochInterval, + validatorIndexes, + `sync_percent >= (${chainAvg} - ${this.config.get('SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG')})`, + ), + ) + ).map((v) => ({ + ...v, + amount: Number(v.amount), + })); } /** @@ -215,24 +288,24 @@ export class ClickhouseService implements OnModuleInit { * how many User Node Operator validators have Sync Committee participation less when chain average last N epoch */ public async getValidatorsCountWithBadSyncParticipationLastNEpoch( - slot: bigint, + epoch: bigint, epochInterval: number, chainAvg: number, validatorIndexes: string[] = [], ): Promise { - const ret = await this.retry(async () => - this.db - .query( - validatorsCountWithSyncParticipationByConditionLastNEpochQuery( - slot, - epochInterval, - validatorIndexes, - `sync_percent < (${chainAvg} - ${this.config.get('SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG')})`, - ), - ) - .toPromise(), - ); - return ret; + return ( + await this.select( + validatorsCountWithSyncParticipationByConditionLastNEpochQuery( + epoch, + epochInterval, + validatorIndexes, + `sync_percent < (${chainAvg} - ${this.config.get('SYNC_PARTICIPATION_DISTANCE_DOWN_FROM_CHAIN_AVG')})`, + ), + ) + ).map((v) => ({ + ...v, + amount: Number(v.amount), + })); } public async getValidatorCountWithPerfectAttestationsLastEpoch(epoch: bigint) { @@ -338,10 +411,14 @@ export class ClickhouseService implements OnModuleInit { condition: string, validatorIndexes: string[] = [], ): Promise { - const ret = await this.retry(async () => - this.db.query(validatorCountByConditionAttestationLastNEpochQuery(epoch, epochInterval, validatorIndexes, condition)).toPromise(), - ); - return ret; + return ( + await this.select( + validatorCountByConditionAttestationLastNEpochQuery(epoch, epochInterval, validatorIndexes, condition), + ) + ).map((v) => ({ + ...v, + amount: Number(v.amount), + })); } /** @@ -351,20 +428,28 @@ export class ClickhouseService implements OnModuleInit { public async getValidatorCountHighAvgIncDelayAttestationOfNEpochQuery( epoch: bigint, ): Promise { - const ret = await this.retry(async () => - this.db.query(validatorCountHighAvgIncDelayAttestationOfNEpochQuery(epoch, this.config.get('BAD_ATTESTATION_EPOCHS'))).toPromise(), - ); - return ret; + return ( + await this.select( + validatorCountHighAvgIncDelayAttestationOfNEpochQuery(epoch, this.config.get('BAD_ATTESTATION_EPOCHS')), + ) + ).map((v) => ({ + ...v, + amount: Number(v.amount), + })); } public async getValidatorsCountWithGoodProposes( epoch: bigint, validatorIndexes: string[] = [], ): Promise { - const ret = await this.retry(async () => - this.db.query(validatorsCountByConditionMissProposeQuery(epoch, validatorIndexes, 'block_proposed = 1')).toPromise(), - ); - return ret; + return ( + await this.select( + validatorsCountByConditionMissProposeQuery(epoch, validatorIndexes, 'block_proposed = 1'), + ) + ).map((v) => ({ + ...v, + amount: Number(v.amount), + })); } /** @@ -375,37 +460,37 @@ export class ClickhouseService implements OnModuleInit { epoch: bigint, validatorIndexes: string[] = [], ): Promise { - const ret = await this.retry(async () => - this.db.query(validatorsCountByConditionMissProposeQuery(epoch, validatorIndexes, 'block_proposed = 0')).toPromise(), - ); - return ret; + return ( + await this.select( + validatorsCountByConditionMissProposeQuery(epoch, validatorIndexes, 'block_proposed = 0'), + ) + ).map((v) => ({ + ...v, + amount: Number(v.amount), + })); } public async getTotalBalance24hDifference(epoch: bigint): Promise { - const ret = await this.retry(async () => this.db.query(totalBalance24hDifferenceQuery(epoch)).toPromise()); + const ret = await this.select<{ curr_total_balance; prev_total_balance; total_diff }[]>(totalBalance24hDifferenceQuery(epoch)); if (ret.length < 1) { return undefined; } - const { curr_total_balance, prev_total_balance, total_diff } = < - { - curr_total_balance: number; - prev_total_balance: number; - total_diff: number; - } - >ret[0]; + const { curr_total_balance, prev_total_balance, total_diff } = ret[0]; if (!curr_total_balance || !prev_total_balance) { return undefined; } - return total_diff; + return Number(total_diff); } - public async getOperatorBalance24hDifference(epoch: bigint): Promise<{ val_nos_name: string; diff: number }[]> { - const ret = await this.retry(async () => this.db.query(operatorBalance24hDifferenceQuery(epoch)).toPromise()); - return <{ val_nos_name: string; diff: number }[]>ret; + public async getOperatorBalance24hDifference(epoch: bigint): Promise<{ val_nos_id; diff }[]> { + return (await this.select<{ val_nos_id; diff }[]>(operatorBalance24hDifferenceQuery(epoch))).map((v) => ({ + ...v, + diff: Number(v.diff), + })); } /** @@ -413,8 +498,12 @@ export class ClickhouseService implements OnModuleInit { * how many User Node Operator validators have active, slashed, pending status */ public async getUserNodeOperatorsStats(epoch: bigint): Promise { - const ret = await this.retry(async () => await this.db.query(userNodeOperatorsStatsQuery(epoch)).toPromise()); - return ret; + return (await this.select(userNodeOperatorsStatsQuery(epoch))).map((v) => ({ + ...v, + active_ongoing: Number(v.active_ongoing), + pending: Number(v.pending), + slashed: Number(v.slashed), + })); } /** @@ -422,8 +511,8 @@ export class ClickhouseService implements OnModuleInit { * how many User Node Operator validators have active, slashed, pending status */ public async getUserValidatorsSummaryStats(epoch: bigint): Promise { - const ret = await this.retry(async () => await this.db.query(userValidatorsSummaryStatsQuery(epoch)).toPromise()); - return ret[0]; + const ret = await this.select(userValidatorsSummaryStatsQuery(epoch)); + return { active_ongoing: Number(ret[0].active_ongoing), pending: Number(ret[0].pending), slashed: Number(ret[0].slashed) }; } /** @@ -431,8 +520,8 @@ export class ClickhouseService implements OnModuleInit { * how many other (not user) validators have active, slashed, pending status */ public async getOtherValidatorsSummaryStats(epoch: bigint): Promise { - const ret = await this.retry(async () => await this.db.query(otherValidatorsSummaryStatsQuery(epoch)).toPromise()); - return ret[0]; + const ret = await this.select(otherValidatorsSummaryStatsQuery(epoch)); + return { active_ongoing: Number(ret[0].active_ongoing), pending: Number(ret[0].pending), slashed: Number(ret[0].slashed) }; } /** @@ -440,12 +529,93 @@ export class ClickhouseService implements OnModuleInit { * User Node Operator proposes stats in the last N epochs */ public async getUserNodeOperatorsProposesStats(epoch: bigint, epochInterval = 120): Promise { - const ret = await this.retry( - async () => - await this.db - .query(userNodeOperatorsProposesStatsLastNEpochQuery(this.config.get('FETCH_INTERVAL_SLOTS'), epoch, epochInterval)) - .toPromise(), - ); - return ret; + return (await this.select(userNodeOperatorsProposesStatsLastNEpochQuery(epoch, epochInterval))).map((v) => ({ + ...v, + all: Number(v.all), + missed: Number(v.missed), + })); + } + + async getEpochMetadata(epoch: bigint): Promise { + const ret = (await this.select(epochMetadata(epoch)))[0]; + const metadata = {}; + if (ret) { + metadata['state'] = { + active_validators: BigInt(ret['active_validators']), + active_validators_total_increments: BigInt(ret['active_validators_total_increments']), + base_reward: Number(ret['base_reward']), + }; + metadata['attestation'] = { + blocks_rewards: new Map(ret['att_blocks_rewards'].map(([b, r]) => [BigInt(b), BigInt(r)])), + participation: { + source: BigInt(ret['att_source_participation']), + target: BigInt(ret['att_target_participation']), + head: BigInt(ret['att_head_participation']), + }, + }; + metadata['sync'] = { + blocks_rewards: new Map(ret['sync_blocks_rewards'].map(([b, r]) => [BigInt(b), BigInt(r)])), + blocks_to_sync: ret['sync_blocks_to_sync'].map((b) => BigInt(b)), + }; + } + return metadata; + } + + public async getOrInitEpochProcessing(epoch: bigint): Promise { + const curr = await this.getEpochProcessing(epoch); + if (curr.epoch == 0n) { + await this.retry( + async () => + await this.db.insert({ + table: 'epochs_processing', + values: [{ epoch }], + format: 'JSONEachRow', + }), + ); + // just for readability + return { epoch: BigInt(epoch), is_stored: undefined, is_calculated: undefined }; + } + return curr; + } + + public async getEpochProcessing(epoch: bigint): Promise { + const ret = (await this.select(epochProcessing(epoch)))[0]; + if (ret) return { ...ret, epoch: BigInt(ret.epoch) }; + return { epoch: 0n, is_stored: undefined, is_calculated: undefined }; + } + + public async getUserNodeOperatorsRewardsAndPenaltiesStats(epoch: bigint): Promise { + return (await this.select(userNodeOperatorsRewardsAndPenaltiesStats(epoch))).map((v) => ({ + ...v, + prop_reward: +v.prop_reward, + prop_missed: +v.prop_missed, + prop_penalty: +v.prop_penalty, + sync_reward: +v.sync_reward, + sync_missed: +v.sync_missed, + sync_penalty: +v.sync_penalty, + att_reward: +v.att_reward, + att_missed: +v.att_missed, + att_penalty: +v.att_penalty, + total_reward: +v.total_reward, + total_missed: +v.total_missed, + total_penalty: +v.total_penalty, + calculated_balance_change: +v.calculated_balance_change, + real_balance_change: +v.real_balance_change, + calculation_error: +v.calculation_error, + })); + } + + public async getAvgChainRewardsAndPenaltiesStats(epoch: bigint): Promise { + return (await this.select(avgChainRewardsAndPenaltiesStats(epoch))).map((v) => ({ + prop_reward: +v.prop_reward, + prop_missed: +v.prop_missed, + prop_penalty: +v.prop_penalty, + sync_reward: +v.sync_reward, + sync_missed: +v.sync_missed, + sync_penalty: +v.sync_penalty, + att_reward: +v.att_reward, + att_missed: +v.att_missed, + att_penalty: +v.att_penalty, + }))[0]; } } diff --git a/src/storage/clickhouse/clickhouse.types.ts b/src/storage/clickhouse/clickhouse.types.ts index 5c93fcf4..9f1d72f8 100644 --- a/src/storage/clickhouse/clickhouse.types.ts +++ b/src/storage/clickhouse/clickhouse.types.ts @@ -5,41 +5,72 @@ export interface ValidatorsStatusStats { } export interface NOsDelta { - val_nos_name: string; + val_nos_id: string; delta: number; } export interface NOsValidatorsNegDeltaCount { - val_nos_name: string; + val_nos_id: string; neg_count: number; } export interface NOsValidatorsSyncAvgPercent { - val_nos_name: string; + val_nos_id: string; avg_percent: number; } export interface NOsValidatorsSyncByConditionCount { - val_nos_name: string; + val_nos_id: string; amount: number; } export interface NOsValidatorsByConditionAttestationCount { - val_nos_name: string; + val_nos_id: string; amount: number; } export interface NOsValidatorsByConditionProposeCount { - val_nos_name: string; + val_nos_id: string; amount: number; } export interface NOsValidatorsStatusStats extends ValidatorsStatusStats { - val_nos_name: string; + val_nos_id: string; +} + +export interface NOsValidatorsRewardsStats { + val_nos_id: string; + prop_reward: number; + prop_missed: number; + prop_penalty: number; + sync_reward: number; + sync_missed: number; + sync_penalty: number; + att_reward: number; + att_missed: number; + att_penalty: number; + total_reward: number; + total_missed: number; + total_penalty: number; + calculated_balance_change: number; + real_balance_change: number; + calculation_error: number; +} + +export interface AvgChainRewardsStats { + prop_reward: number; + prop_missed: number; + prop_penalty: number; + sync_reward: number; + sync_missed: number; + sync_penalty: number; + att_reward: number; + att_missed: number; + att_penalty: number; } export interface NOsProposesStats { - val_nos_name: string; + val_nos_id: string; all: number; missed: number; } @@ -47,3 +78,9 @@ export interface NOsProposesStats { export interface SyncCommitteeParticipationAvgPercents { avg_percent: number; } + +export interface EpochProcessingState { + epoch: bigint; + is_stored?: boolean; + is_calculated?: boolean; +} diff --git a/src/storage/clickhouse/migrations/migration_000002_rewards.ts b/src/storage/clickhouse/migrations/migration_000002_rewards.ts new file mode 100644 index 00000000..5c35c806 --- /dev/null +++ b/src/storage/clickhouse/migrations/migration_000002_rewards.ts @@ -0,0 +1,19 @@ +const sql = ` +ALTER TABLE validators_summary +// state +ADD COLUMN IF NOT EXISTS val_effective_balance Nullable(UInt64) AFTER val_balance, +// att +ADD COLUMN IF NOT EXISTS att_earned_reward Nullable(UInt64) AFTER att_valid_source, +ADD COLUMN IF NOT EXISTS att_missed_reward Nullable(UInt64) AFTER att_earned_reward, +ADD COLUMN IF NOT EXISTS att_penalty Nullable(UInt64) AFTER att_missed_reward, +// sync +ADD COLUMN IF NOT EXISTS sync_earned_reward Nullable(UInt64) AFTER sync_percent, +ADD COLUMN IF NOT EXISTS sync_missed_reward Nullable(UInt64) AFTER sync_earned_reward, +ADD COLUMN IF NOT EXISTS sync_penalty Nullable(UInt64) AFTER sync_missed_reward, +// propose +ADD COLUMN IF NOT EXISTS propose_earned_reward Nullable(UInt64) AFTER block_to_propose, +ADD COLUMN IF NOT EXISTS propose_missed_reward Nullable(UInt64) AFTER propose_earned_reward, +ADD COLUMN IF NOT EXISTS propose_penalty Nullable(UInt64) AFTER propose_missed_reward +`; + +export default sql; diff --git a/src/storage/clickhouse/migrations/migration_000003_epoch_meta.ts b/src/storage/clickhouse/migrations/migration_000003_epoch_meta.ts new file mode 100644 index 00000000..2fa1fb14 --- /dev/null +++ b/src/storage/clickhouse/migrations/migration_000003_epoch_meta.ts @@ -0,0 +1,18 @@ +const sql = ` +CREATE TABLE IF NOT EXISTS epochs_metadata ( + "epoch" Int64, + "active_validators" UInt32, + "active_validators_total_increments" Int64, + "base_reward" UInt32, + "att_blocks_rewards" Array(Array(Int64)), + "att_source_participation" Int64, + "att_target_participation" Int64, + "att_head_participation" Int64, + "sync_blocks_rewards" Array(Array(Int64)), + "sync_blocks_to_sync" Array(Int64), + INDEX epoch_index (epoch) TYPE minmax GRANULARITY 8192 +) +ENGINE = ReplacingMergeTree() +ORDER BY epoch +`; +export default sql; diff --git a/src/storage/clickhouse/migrations/migration_000004_epoch_processing.ts b/src/storage/clickhouse/migrations/migration_000004_epoch_processing.ts new file mode 100644 index 00000000..6968d961 --- /dev/null +++ b/src/storage/clickhouse/migrations/migration_000004_epoch_processing.ts @@ -0,0 +1,10 @@ +const sql = ` +CREATE TABLE IF NOT EXISTS epochs_processing ( + "epoch" Int64, + "is_stored" Nullable(UInt8), + "is_calculated" Nullable(UInt8) +) +ENGINE = ReplacingMergeTree() +ORDER BY epoch +`; +export default sql; diff --git a/test/duties.e2e-spec.ts b/test/duties.e2e-spec.ts index 0c6155e5..1cf96ea1 100644 --- a/test/duties.e2e-spec.ts +++ b/test/duties.e2e-spec.ts @@ -1,3 +1,5 @@ +import * as process from 'process'; + import { getNetwork } from '@ethersproject/providers'; import { createMock } from '@golevelup/ts-jest'; import { FallbackProviderModule, SimpleFallbackJsonRpcBatchProvider } from '@lido-nestjs/execution'; @@ -16,7 +18,6 @@ import { ClickhouseService } from 'storage'; import { ValStatus } from '../src/common/eth-providers'; import { DutyModule, DutyService } from '../src/duty'; -import { ValidatorDutySummary } from '../src/duty/summary'; const MikroORMMockProvider = { provide: MikroORM, @@ -77,6 +78,16 @@ const testSyncMember = { att_valid_head: true, att_valid_target: true, att_valid_source: true, + // rewards + att_earned_reward: 14270n, + att_missed_reward: 0n, + att_penalty: 0n, + val_effective_balance: 32000000000n, + sync_earned_reward: 362525n, + sync_missed_reward: 101507n, + sync_penalty: 101507n, + sync_meta: undefined, + att_meta: undefined, }, }; @@ -105,23 +116,30 @@ const testProposerMember = { att_valid_head: true, att_valid_target: true, att_valid_source: true, + // rewards + att_earned_reward: 14270n, + att_missed_reward: 0n, + att_penalty: 0n, + val_effective_balance: 32000000000n, + sync_meta: undefined, + att_meta: undefined, }, }; const testValidators = [testSyncMember, testProposerMember]; -const testValidatorIndexes = testValidators.map((v) => v.index); describe('Duties', () => { - jest.setTimeout(240 * 1000); + jest.setTimeout(360 * 1000); let dutyService: DutyService; let validatorsRegistryService: RegistryService; let clickhouseService: ClickhouseService; let epochNumber, stateSlot; - let indexesToSave: string[]; - let summaryToSave: ValidatorDutySummary[]; + const indexesToSave = []; + let summaryToSave = []; + process.env['DB_HOST'] = 'http://localhost'; // stub to avoid lib validator const getActualKeysIndexedMock = jest.fn().mockImplementation(async () => { const map = new Map(); testValidators.forEach((v) => @@ -135,7 +153,14 @@ describe('Duties', () => { return map; }); jest.spyOn(SimpleFallbackJsonRpcBatchProvider.prototype, 'detectNetwork').mockImplementation(async () => getNetwork('mainnet')); - const writeIndexesSpy = jest.spyOn(ClickhouseService.prototype, 'writeIndexes'); + jest.spyOn(ClickhouseService.prototype, 'writeIndexes').mockImplementation( + async (pipeline): Promise => + await new Promise((resolve, reject) => { + pipeline.on('data', (data) => indexesToSave.push(data)); + pipeline.on('error', (e) => reject(e)); + pipeline.on('end', () => resolve(true)); + }).finally(() => pipeline.destroy()), + ); jest.spyOn(ClickhouseService.prototype, 'writeSummary'); beforeAll(async () => { @@ -169,13 +194,12 @@ describe('Duties', () => { // stub writing to db Object.defineProperty(clickhouseService, 'db', { value: { - insert: () => ({ - stream: () => ({ - writeRow: async () => [], - exec: async () => [], - destroy: async () => [], - }), - }), + insert: () => [], + query: () => { + return { + json: () => [], + }; + }, }, }); @@ -183,9 +207,8 @@ describe('Duties', () => { epochNumber = BigInt(process.env['TEST_EPOCH_NUMBER']); await Promise.all([dutyService['prefetch'](epochNumber), dutyService['checkAll'](epochNumber, stateSlot)]); - summaryToSave = dutyService['summary'].values(); - indexesToSave = writeIndexesSpy.mock.calls[0][0].map((i) => i.index); - await dutyService['write'](); + summaryToSave = [...dutyService['summary'].values()].map((v) => ({ ...v, att_meta: undefined, sync_meta: undefined })); + await dutyService['writeSummary'](); }); describe('should be processes validators info', () => { @@ -198,7 +221,11 @@ describe('Duties', () => { }); it('indexes content to save should contains all tested validators', () => { - testValidatorIndexes.forEach((i) => expect(indexesToSave).toContain(String(i))); + testValidators.forEach((i) => { + const toSaveTestedIndex = indexesToSave.find((v) => v.val_id == String(i.index)); + expect(toSaveTestedIndex).toBeDefined(); + expect(toSaveTestedIndex).toEqual({ val_id: String(i.index), val_pubkey: i.pubkey }); + }); }); it('summary content to save should contains right tested sync validator performance info', () => { diff --git a/yarn.lock b/yarn.lock index 1375218a..9877f57e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -360,15 +360,22 @@ dependencies: "@chainsafe/as-sha256" "^0.3.1" -"@chainsafe/ssz@^0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.2.tgz#6f2552db312217b911e34bcdef9057f8a3a108f2" - integrity sha512-r3bKiGMF7EZlsgXTyyzQbS+GJTj6MvTlY3Ms1byFZLL1H9Maht8muE2LkF3pS1zU9KY4tiJeQd+KABdhyfB9Ag== +"@chainsafe/ssz@^0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.4.tgz#696a8db46d6975b600f8309ad3a12f7c0e310497" + integrity sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ== dependencies: "@chainsafe/as-sha256" "^0.3.1" "@chainsafe/persistent-merkle-tree" "^0.4.2" case "^1.6.3" +"@clickhouse/client@^0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-0.0.11.tgz#275ee95ac2596056584981ee8d1afc58ea5652f6" + integrity sha512-ZxabGakXyRmtKqjyFKHtKrWcNsTdVmeHCZF4JuXHS72m+YmAjTUdAasaHN6xkqRzhkw/Dj94SBxWWTi5gg+ClA== + dependencies: + node-abort-controller "^3.0.1" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -390,11 +397,6 @@ enabled "2.0.x" kuler "^2.0.0" -"@discoveryjs/json-ext@^0.5.7": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - "@eslint/eslintrc@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" @@ -1893,14 +1895,6 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -JSONStream@1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.4.tgz#615bb2adb0cd34c8f4c447b5f6512fa1d8f16a2e" - integrity sha512-Y7vfi3I5oMOYIr+WxV8NZxDSwcbNgzdKYsTNInmycOq9bUYwGg9ryu57Wg5NLmCjqdFPNUmpMBo3kSJN9tCbXg== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - abab@^2.0.3, abab@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -2012,7 +2006,7 @@ ajv@8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2179,18 +2173,6 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== - async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -2215,16 +2197,6 @@ avvio@^8.1.3: debug "^4.0.0" fastq "^1.6.1" -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - axios@0.27.2: version "0.27.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" @@ -2304,13 +2276,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - bech32@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" @@ -2502,11 +2467,6 @@ case@^1.6.3: resolved "https://registry.yarnpkg.com/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9" integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== - chalk@2.4.2, chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2626,20 +2586,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -clickhouse@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/clickhouse/-/clickhouse-2.6.0.tgz#6218e3978fd5f343bdff395d988c5cd44e8b9a03" - integrity sha512-HC5OV99GJOup4qZsTuWWPpXlj+847Z0OeygDU2x22rNYost0V/vWapzFWYZdV/5iRbGMrhFQPOyQEzmGvoaWRQ== - dependencies: - JSONStream "1.3.4" - lodash "4.17.21" - querystring "0.2.0" - request "2.88.0" - stream2asynciter "1.0.3" - through "2.3.8" - tsv "0.2.0" - uuid "3.4.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -2729,7 +2675,7 @@ colorspace@1.1.x: color "^3.1.3" text-hex "1.0.x" -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2805,11 +2751,6 @@ cookiejar@^2.1.3: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - cosmiconfig@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" @@ -2859,13 +2800,6 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== - dependencies: - assert-plus "^1.0.0" - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -3056,14 +2990,6 @@ dotenv@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - electron-to-chromium@^1.4.202, electron-to-chromium@^1.4.251: version "1.4.272" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.272.tgz#cedebaeec5d9879da85b127e65a55c6b4c58344e" @@ -3500,11 +3426,6 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -3514,16 +3435,6 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3722,11 +3633,6 @@ follow-redirects@^1.14.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - fork-ts-checker-webpack-plugin@7.2.11: version "7.2.11" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz#aff3febbc11544ba3ad0ae4d5aa4055bd15cd26d" @@ -3762,15 +3668,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - formidable@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" @@ -3948,13 +3845,6 @@ getopts@2.3.0: resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" - glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -4087,19 +3977,6 @@ growl@1.10.5: resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.0: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -4199,15 +4076,6 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - http2-wrapper@^1.0.0-beta.5.2: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -4533,7 +4401,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== @@ -4562,11 +4430,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" @@ -5054,11 +4917,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -5117,30 +4975,20 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - json5@2.x, json5@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -5165,21 +5013,6 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - keyv@^4.0.0: version "4.3.3" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.3.3.tgz#6c1bcda6353a9e96fc1b4e1aeb803a6e35090ba9" @@ -5329,9 +5162,9 @@ lru-cache@^6.0.0: yallist "^4.0.0" luxon@^1.23.x: - version "1.28.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" - integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== + version "1.28.1" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.1.tgz#528cdf3624a54506d710290a2341aa8e6e6c61b0" + integrity sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw== macos-release@^2.5.0: version "2.5.0" @@ -5426,7 +5259,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@^2.1.27: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -5477,11 +5310,16 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@1.2.6, minimist@^1.2.0, minimist@^1.2.6: +minimist@1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -5623,6 +5461,11 @@ nest-winston@^1.6.2: dependencies: fast-safe-stringify "^2.1.1" +node-abort-controller@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" + integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== + node-addon-api@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" @@ -5717,11 +5560,6 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -5950,11 +5788,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - pg-connection-string@2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" @@ -6130,7 +5963,7 @@ proxy-addr@^2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -psl@^1.1.24, psl@^1.1.33: +psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -6143,11 +5976,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -6165,16 +5993,6 @@ qs@^6.10.3: dependencies: side-channel "^1.0.4" -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== - querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -6273,32 +6091,6 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -request@2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -6423,7 +6215,7 @@ rxjs@^7.2.0: dependencies: tslib "^2.1.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -6454,7 +6246,7 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -6688,21 +6480,6 @@ sqlstring@2.3.3: resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== -sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - ssri@^8.0.0, ssri@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" @@ -6722,10 +6499,17 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -stream2asynciter@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/stream2asynciter/-/stream2asynciter-1.0.3.tgz#7ba9046846c8b1caf36ec30d64a73514f7f44c5a" - integrity sha512-9/dEZW+LQjuW6ub5hmWi4n9Pn8W8qA8k7NAE1isecesA164e73xTdy1CJ3S9o9YS+O21HuiK7T+4uS7FgKDy4w== +stream-chain@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09" + integrity sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA== + +stream-json@^1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/stream-json/-/stream-json-1.7.5.tgz#2ff0563011f22cea4f6a28dbfc0344a53c761fe4" + integrity sha512-NSkoVduGakxZ8a+pTPUlcGEeAGQpWL9rKJhOFCV+J/QtdQUEU5vtBgVg6eJXn8JB8RZvpbJWZGvXkhz70MLWoA== + dependencies: + stream-chain "^2.2.5" string-length@^4.0.1: version "4.0.2" @@ -6970,7 +6754,7 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== -through@2.3.8, "through@>=2.2.7 <3", through@^2.3.6: +through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -7027,14 +6811,6 @@ tough-cookie@^4.0.0: universalify "^0.2.0" url-parse "^1.5.3" -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -7141,23 +6917,6 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tsv@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/tsv/-/tsv-0.2.0.tgz#92869a3cb5f50332f3dc90fca82be667db6f72d6" - integrity sha512-GG6xbOP85giXXom0dS6z9uyDsxktznjpa1AuDlPrIXDqDnbhjr9Vk6Us8iz6U1nENL4CPS2jZDvIjEdaZsmc4Q== - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -7287,11 +7046,6 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@3.4.0, uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - uuid@8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -7326,15 +7080,6 @@ vary@^1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"