From b84097c6d08ca2bbfc34826805ffe612208d6e4b Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 14:55:28 +0100 Subject: [PATCH 01/10] no exact version pinning for metadata --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 72e528ce..2bbcb004 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@decentral.ee/web3-helpers": "^0.5.3", "@slack/webhook": "^6.1.0", "@superfluid-finance/ethereum-contracts": "1.9.0", - "@superfluid-finance/metadata": "1.1.27", + "@superfluid-finance/metadata": "^1.1.27", "async": "^3.2.4", "axios": "^1.4.0", "bip39": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index bb1ad1de..3b06ad80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1109,10 +1109,10 @@ ethereumjs-util "7.1.5" hardhat "^2.19.4" -"@superfluid-finance/metadata@1.1.27": - version "1.1.27" - resolved "https://registry.yarnpkg.com/@superfluid-finance/metadata/-/metadata-1.1.27.tgz#24df39277e6eb27201765693bbc47ce4c93befb6" - integrity sha512-61Dj7zcncPLSyqhWowj6Xmb6xeeT9b5Yjg4lAhnFIk1XCiycxrgsWc7aokbm4On3dLRg3xBiacy6qoG2rSaM5A== +"@superfluid-finance/metadata@^1.1.27": + version "1.1.28" + resolved "https://npm.pkg.github.com/download/@superfluid-finance/metadata/1.1.28/0724432be7486c1fe1f147591f15c1b41391267a#0724432be7486c1fe1f147591f15c1b41391267a" + integrity sha512-MbhcHjcVahQZAdZZWBIzeVIFBScgksaejKWy8/hcShymYlo/0fHB2g90ty+vQAc7bZEquHOkRXfgGoP65D+KgQ== "@szmarczak/http-timer@^4.0.5": version "4.0.6" From 6b35ff0a399429840f276a06b34cbd0ed3dce504 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 14:55:53 +0100 Subject: [PATCH 02/10] fix observer mode --- src/app.js | 8 +++++--- src/httpserver/report.js | 10 ++++++---- src/services/notificationJobs.js | 18 ++++++++++-------- src/services/telemetry.js | 2 +- src/web3client/client.js | 2 +- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/app.js b/src/app.js index 58bc7838..2ba7a8df 100644 --- a/src/app.js +++ b/src/app.js @@ -194,9 +194,11 @@ class App { } // create all web3 infrastructure needed await this.client.init(); - const balanceMsg = `RPC connected with chainId ${await this.client.getChainId()}, account ${this.client.accountManager.getAccountAddress(0)} has balance ${wad4human(await this.client.accountManager.getAccountBalance(0))}`; + const balanceMsg = `RPC connected with chainId ${await this.client.getChainId()}` + + this.config.OBSERVER ? "" : + `account ${this.client.accountManager.getAccountAddress(0)} has balance ${wad4human(await this.client.accountManager.getAccountBalance(0))}`; this.notifier.sendNotification(balanceMsg); - + //check conditions to decide if getting snapshot data if ((!dbFileExist || this.config.COLD_BOOT) && this.config.FASTSYNC && this.config.CID) { @@ -245,7 +247,7 @@ class App { // update thresholds on database await this.db.sysQueries.updateThresholds(tokensThresholds.thresholds); } catch (err) { - this.logger.warn(`error loading thresholds.json`); + this.logger.warn(`thresholds.json not loaded`); await this.db.sysQueries.updateThresholds({}); this.config.SENTINEL_BALANCE_THRESHOLD = 0; } diff --git a/src/httpserver/report.js b/src/httpserver/report.js index 83026741..f1b3eed5 100644 --- a/src/httpserver/report.js +++ b/src/httpserver/report.js @@ -80,10 +80,12 @@ class Report { } }, - account: { - address: this.app.client.getAccountAddress(), - balance: (await this.app.client.getAccountBalance()).toString(), - }, + ...(this.app.config.OBSERVER ? {} : { + account: { + address: this.app.client.getAccountAddress(), + balance: (await this.app.client.getAccountBalance()).toString(), + } + }), queues: { agreementQueue: this.app.queues.getAgreementQueueLength(), diff --git a/src/services/notificationJobs.js b/src/services/notificationJobs.js index 72cf6bbf..2c341c74 100644 --- a/src/services/notificationJobs.js +++ b/src/services/notificationJobs.js @@ -15,17 +15,19 @@ class NotificationJobs { async sendReport () { const healthcheck = await this.app.healthReport.fullReport(); - if(!healthcheck.healthy) { + if (!healthcheck.healthy) { const healthData = `Instance Name: ${this.app.config.INSTANCE_NAME}\nHealthy: ${healthcheck.healthy}\nChainId: ${healthcheck.network.chainId}\nReasons: ${healthcheck.reasons.join('\n')}`; this.app.notifier.sendNotification(healthData); } - const currentTime = Date.now(); - if(currentTime - this._lastBalanceReportTime >= BALANCE_REPORT_INTERVAL) { - const balanceQuery = await this.app.client.isAccountBalanceBelowMinimum(); - if(balanceQuery.isBelow) { - this.app.notifier.sendNotification(`Attention: Sentinel balance: ${wad4human(balanceQuery.balance)}`); - // update the time of last balance report - this._lastBalanceReportTime = currentTime; + if (!this.app.config.OBSERVER) { + const currentTime = Date.now(); + if(currentTime - this._lastBalanceReportTime >= BALANCE_REPORT_INTERVAL) { + const balanceQuery = await this.app.client.isAccountBalanceBelowMinimum(); + if(balanceQuery.isBelow) { + this.app.notifier.sendNotification(`Attention: Sentinel balance: ${wad4human(balanceQuery.balance)}`); + // update the time of last balance report + this._lastBalanceReportTime = currentTime; + } } } } diff --git a/src/services/telemetry.js b/src/services/telemetry.js index f8aace62..6351967b 100644 --- a/src/services/telemetry.js +++ b/src/services/telemetry.js @@ -85,7 +85,7 @@ sentinel_telemetry_healthy{${labels}} ${healthReport.healthy ? 1 : 0} # TYPE sentinel_telemetry_rpc_requests counter sentinel_telemetry_rpc_requests{${labels}} ${healthReport.network.rpc.totalRequests} ` -+ (healthReport.account.balance ? // undefined in observer mode ++ (healthReport.account?.balance ? // undefined in observer mode `# HELP sentinel_telemetry_account_balance Balance of the monitored account, rounded to 3 decimal places. # TYPE sentinel_telemetry_account_balance gauge sentinel_telemetry_account_balance{${labels}} ${Math.floor(parseInt(healthReport.account.balance) / 1e15) / 1e3} diff --git a/src/web3client/client.js b/src/web3client/client.js index fda9f9a3..03131531 100644 --- a/src/web3client/client.js +++ b/src/web3client/client.js @@ -66,7 +66,7 @@ class Client { } else if(this.app.config.OBSERVER) { this.app.logger.warn("Configuration is set to be Observer."); } else { - throw Error("No account configured. Either PRIVATE_KEY or MNEMONIC needs to be set."); + throw Error("No account configured. Either PRIVATE_KEY or MNEMONIC needs to be set. Set OBSERVER=true if you really want to run a passive sentinel."); } if(!this.app.config.OBSERVER) { From 9edc2db20d34f0ec566c6ddc11eb1e4ce5e57ffb Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 15:24:08 +0100 Subject: [PATCH 03/10] use public repo for metadata --- .env-example => .env.example | 0 yarn.lock | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .env-example => .env.example (100%) diff --git a/.env-example b/.env.example similarity index 100% rename from .env-example rename to .env.example diff --git a/yarn.lock b/yarn.lock index 3b06ad80..3c8220b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,7 +1111,7 @@ "@superfluid-finance/metadata@^1.1.27": version "1.1.28" - resolved "https://npm.pkg.github.com/download/@superfluid-finance/metadata/1.1.28/0724432be7486c1fe1f147591f15c1b41391267a#0724432be7486c1fe1f147591f15c1b41391267a" + resolved "https://registry.yarnpkg.com/@superfluid-finance/metadata/-/metadata-1.1.28.tgz#0724432be7486c1fe1f147591f15c1b41391267a" integrity sha512-MbhcHjcVahQZAdZZWBIzeVIFBScgksaejKWy8/hcShymYlo/0fHB2g90ty+vQAc7bZEquHOkRXfgGoP65D+KgQ== "@szmarczak/http-timer@^4.0.5": From 0efa93c967c0af5feb745ff4f6c1ddb0d1c2deee Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 17:33:15 +0100 Subject: [PATCH 04/10] report app version in log --- src/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index 2ba7a8df..e8d0b132 100644 --- a/src/app.js +++ b/src/app.js @@ -24,6 +24,7 @@ const Telemetry = require("./services/telemetry"); const Errors = require("./utils/errors/errors"); const CircularBuffer = require("./utils/circularBuffer"); const { wad4human } = require("@decentral.ee/web3-helpers"); +const packageVersion = require("../package.json").version; class App { /* @@ -178,7 +179,7 @@ class App { async start() { try { - this.logger.debug(`booting - ${this.config.INSTANCE_NAME}`); + this.logger.debug(`booting version ${packageVersion} - ${this.config.INSTANCE_NAME}`); this._isShutdown = false; // send notification about time sentinel started including timestamp this.notifier.sendNotification(`Sentinel started at ${new Date()}`); From 82cd27f0d6e8fb5cde292b28401530a5a85ebc0e Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 17:33:30 +0100 Subject: [PATCH 05/10] bump version to 1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2bbcb004..96fd1e4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "superfluid-sentinel", - "version": "0.11.0", + "version": "1.0.0", "description": "Superfluid Sentinel", "main": "main.js", "scripts": { From 6d9780e5fb570461291c0e3ab3f845071c073ff6 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 17:55:14 +0100 Subject: [PATCH 06/10] simplified docker config --- Dockerfile | 7 ++----- docker-compose-with-monitoring.yml | 19 ++++--------------- docker-compose.yml | 10 ++-------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9caf2bf4..0d7420c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,8 @@ WORKDIR /app # Install dependencies RUN apk add --update --no-cache \ - g++ \ - make \ - python3 \ - yarn \ - tini && ln -sf python3 /usr/bin/python + yarn \ + tini # Copy package.json and yarn.lock COPY package.json yarn.lock ./ diff --git a/docker-compose-with-monitoring.yml b/docker-compose-with-monitoring.yml index 7dbcdfc5..e87e6d3f 100644 --- a/docker-compose-with-monitoring.yml +++ b/docker-compose-with-monitoring.yml @@ -1,13 +1,11 @@ # Starts the Sentinel service and connected monitoring services: Prometheus and Grafana. version: '3' -networks: - monitoring: - driver: bridge services: # the sentinel image is built from source sentinel: + image: superfluidfinance/superfluid-sentinel build: . restart: unless-stopped env_file: .env @@ -22,8 +20,6 @@ services: - 9100 volumes: - data:/app/data - networks: - - monitoring deploy: resources: limits: @@ -33,7 +29,6 @@ services: memory: 50M prometheus: image: prom/prometheus:v2.36.1 - container_name: prometheus volumes: - ./prometheus:/etc/prometheus - prometheus_data:/prometheus @@ -41,11 +36,8 @@ services: - ${PROMETHEUS_PORT:-9090}:9090 expose: - ${PROMETHEUS_PORT:-9090} - networks: - - monitoring grafana: image: grafana/grafana:8.2.6 - container_name: grafana volumes: - grafana_data:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning @@ -55,11 +47,8 @@ services: - ${GRAFANA_PORT:-3000}:3000 expose: - ${GRAFANA_PORT:-3000} - networks: - - monitoring volumes: - prometheus_data: { } - grafana_data: { } - data: { } - + prometheus_data: + grafana_data: + data: diff --git a/docker-compose.yml b/docker-compose.yml index 20256344..da5e011a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,10 @@ -# Minimal version which starts the sentinel service only, without additional monitoring services. +# Basic docker-compose file for running a sentinel. # This is ideal for resource constrained environments or for use with custom monitoring setups. version: '3' services: sentinel: + image: superfluidfinance/superfluid-sentinel:${SENTINEL_VERSION:-latest} build: . restart: unless-stopped env_file: .env @@ -16,13 +17,6 @@ services: - ${METRICS_PORT:-9100}:9100 volumes: - data:/app/data - deploy: - resources: - limits: - cpus: '0.50' - memory: 300M - reservations: - memory: 50M volumes: data: From 7b05ed74affe401dbbd77059db7372966b4e73e9 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 18:00:30 +0100 Subject: [PATCH 07/10] move SENTINEL_BALANCE_THRESHOLD to config --- src/app.js | 2 -- src/config/configuration.js | 7 +++++-- thresholds.json => thresholds.json.example | 3 +-- 3 files changed, 6 insertions(+), 6 deletions(-) rename thresholds.json => thresholds.json.example (79%) diff --git a/src/app.js b/src/app.js index e8d0b132..bcce19f0 100644 --- a/src/app.js +++ b/src/app.js @@ -244,13 +244,11 @@ class App { try { const thresholds = require("../thresholds.json"); const tokensThresholds = thresholds.networks[await this.client.getChainId()]; - this.config.SENTINEL_BALANCE_THRESHOLD = tokensThresholds.minSentinelBalanceThreshold; // update thresholds on database await this.db.sysQueries.updateThresholds(tokensThresholds.thresholds); } catch (err) { this.logger.warn(`thresholds.json not loaded`); await this.db.sysQueries.updateThresholds({}); - this.config.SENTINEL_BALANCE_THRESHOLD = 0; } diff --git a/src/config/configuration.js b/src/config/configuration.js index efa7936d..1c7de76a 100644 --- a/src/config/configuration.js +++ b/src/config/configuration.js @@ -67,6 +67,7 @@ class Config { this.PIRATE = this._parseToBool(config.pirate); this.INSTANCE_NAME = config.INSTANCE_NAME || "Sentinel"; this.RPC_STUCK_THRESHOLD = config.rpc_stuck_threshold; + this.SENTINEL_BALANCE_THRESHOLD = config.sentinel_balance_threshold; } _initializeFromEnvVariables() { @@ -112,8 +113,9 @@ class Config { this.BLOCK_OFFSET = process.env.BLOCK_OFFSET || 12; this.MAX_TX_NUMBER = process.env.MAX_TX_NUMBER || 100; this.NO_REMOTE_MANIFEST = this._parseToBool(process.env.NO_REMOTE_MANIFEST, false); - this.INSTANCE_NAME = process.env.INSTANCE_NAME || "Sentinel"; this.RPC_STUCK_THRESHOLD = process.env.RPC_STUCK_THRESHOLD || (this.POLLING_INTERVAL * 4) / 1000; + this.INSTANCE_NAME = process.env.INSTANCE_NAME || "Sentinel"; + this.SENTINEL_BALANCE_THRESHOLD = process.env.SENTINEL_BALANCE_THRESHOLD || 0; } _parseToBool(value, defaultValue = false) { @@ -183,7 +185,6 @@ class Config { getConfigurationInfo () { return { - INSTANCE_NAME: this.INSTANCE_NAME, HTTP_RPC_NODE: this.HTTP_RPC_NODE, FASTSYNC: this.FASTSYNC, OBSERVER: this.OBSERVER, @@ -214,6 +215,8 @@ class Config { SLACK_WEBHOOK_URL: this.SLACK_WEBHOOK_URL, TELEGRAM_BOT_TOKEN: this.TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID: this.TELEGRAM_CHAT_ID, + INSTANCE_NAME: this.INSTANCE_NAME, + SENTINEL_BALANCE_THRESHOLD: this.SENTINEL_BALANCE_THRESHOLD, }; } } diff --git a/thresholds.json b/thresholds.json.example similarity index 79% rename from thresholds.json rename to thresholds.json.example index c884001f..2e71642a 100644 --- a/thresholds.json +++ b/thresholds.json.example @@ -4,11 +4,10 @@ "networks": { "137": { "name": "Polygon", - "minSentinelBalanceThreshold": "100000000000000000000", "thresholds": [{ "address": "0xCAa7349CEA390F89641fe306D93591f87595dc1F", "above": "3858024691" }] } } -} \ No newline at end of file +} From 17511b93a789d7818cb6bacb460b10a391452c67 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 18:00:58 +0100 Subject: [PATCH 08/10] updated readme and example env --- .env.example | 11 ++- .gitignore | 2 +- README.md | 223 ++++++++++++++++++++++++++++----------------------- 3 files changed, 130 insertions(+), 106 deletions(-) diff --git a/.env.example b/.env.example index f09cd32c..b3a440e7 100644 --- a/.env.example +++ b/.env.example @@ -120,16 +120,19 @@ HTTP_RPC_NODE= ## When running with Docker, this will affect the host port binding, not the binding inside the container. #METRICS_PORT=9100 -# Let the sentinel instance periodically report a basic metrics to a remote server. -# Set this to false in order to disable it. +## Let the sentinel instance periodically report a basic metrics to a remote server. +## Set this to false in order to disable it. #TELEMETRY=true -# Default telemetry server instance provided by Superfluid +## Default telemetry server instance provided by Superfluid #TELEMETRY_URL=https://sentinel-telemetry.x.superfluid.dev -# Reporting interval, defaults to 12 hours +## Reporting interval, defaults to 12 hours #TELEMETRY_INTERVAL=43200 +## Allows to set a custom instance name, included in the data sent to the telemetry server. +#INSTANCE_NAME=Sentinel + ## If set, you get notified about key events like process (re)starts, configuration changes and error conditions ## to the Slack channel the hook belongs to. #SLACK_WEBHOOK_URL= diff --git a/.gitignore b/.gitignore index e976277d..7e91f257 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ artifacts database.sqlite .env .env* -!.env-example +!.env.example .DS_Store *.sqlite .npmrc diff --git a/README.md b/README.md index d343101d..e7f21d4c 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,86 @@ # Superfluid Sentinel The sentinel monitors the state of Superfluid agreements on the configured network and -liquidates [critical agreements](https://docs.superfluid.finance/superfluid/docs/constant-flow-agreement#liquidation-and-solvency). -It also allows you to configure a related PIC account and other parameters related to [3Ps & TOGA](https://docs.superfluid.finance/superfluid/docs/liquidations-and-toga). +liquidates [critical agreements](https://docs.superfluid.finance/docs/protocol/advanced-topics/solvency/liquidations-and-toga#liquidation-and-solvency). +It also allows you to configure a related PIC account and other parameters related to [3Ps & TOGA](https://docs.superfluid.finance/docs/protocol/advanced-topics/solvency/liquidations-and-toga#patricians-plebs-and-pirates-3ps). -## Quickstart +## How to run a Sentinel Currently supported setups: -* Native * Docker +* Native ### Prerequisites -First, prepare an account funded with native coins for transaction fees. +First, prepare an account funded with native coins for transaction fees. Then prepare a file `.env` with your configuration. You can start with the provided example: ``` -cp .env-example .env +cp .env.example .env ``` The following configuration items are required and don't have default values: * `HTTP_RPC_NODE` (cmdline argument: `-H`) -* `PRIVATE_KEY` (cmdline argument: `-k`) or MNEMONIC (cmdline argument: `-m`) +* `PRIVATE_KEY` (cmdline argument: `-k`) or `MNEMONIC` (cmdline argument: `-m`) In order to associate a sentinel instance with a PIC account, set the `PIC` env variable (cmdline argument: `--pic`). Check `.env.example` for additional configuration items and their documentation. +### Docker Setup + +This part of the guide assumes you have a recent version of Docker and Compose v2 installed. + +1. Create a directory and enter it +``` +mkdir superfluid-sentinel && cd superfluid-sentinel +``` + +2. Create a file `docker-compose.yml`. You can copy one contained in this repository or write one yourself. + +3. Create a file (see prerequisites section) + +4. Start the application with `docker compose up` (add ` -d` to run in the background) + +This should pull the latest sentinel image and start it with the provided configuration. + +An sqlite DB is stored in a Docker volume named `superfluid-sentinel_data` (can differ based on the name of your local +directory). + +Use `docker compose logs` in order to see the logs in this case (add `-f` to follow the live log). + +#### Update + +In order to update to the latest published image, run `docker compose pull`, then restart the application with `docker compose up -d`. + +Sometimes you may need to re-sync the sentinel DB. That is the case if you change network (different chainId) or if the DB schema changes. +In this case, first shutdown the container with `docker compose down`. +Then either identify and delete the data volume, or add a config flag `COLD_BOOT=1` to `.env` and then run with `docker compose up`. This tells the sentinel to drop the previous DB and rebuild it from scratch. +As soon as you see the sentinel syncing, you can again shut it down, remove the flag from `.env` and start it again. + +Caution: don't forget to remove the flag `COLD_BOOT` from your config, otherwise the sentinel will rebuild the DB on every restart, which may considerably delay its operation and consume a lot of RPC requests. + +#### Local build + +Instead of using a published Docker image, you can also build your own image and use it. +In order to do so, clone this repository and create an `.env` file, then run +``` +docker compose up --build +``` + +#### Bundled monitoring services + +If you want to run the sentinel bundled with Prometheus and Grafana, use the compose file `docker-compose-with-monitoring.yml` instead of the default one. +In order to do so, you can either set `COMPOSE_FILE=docker-compose-with-monitoring.yml` in your .env, or copy and rename to `docker-compose.yml`. + +When running this, you can access a Grafana dashboard at the configured port (default: 3000). +The initial credentials are admin:admin, you will be asked to change the password on first login. + +A sentinel specific dashboard is not yet included, but you can already select a generic dashboard for node.js applications. +Work is in progress for adding more sentinel specific metrics to the prometheus exporter of the sentinel application which feeds Grafana. + ### Native Setup Requires Node.js v18+ and yarn already installed. @@ -77,47 +129,14 @@ If all is well, you may want to set the service to autostart: systemctl enable superfluid-sentinel.service ``` -### Monitoring, Alerting & Telemetry - -The sentinel can provide monitoring information. In the default configuration, this is available on port 9100 and json formatted. - -The endpoint `/` returns a status summary, including a flag `healthy` which turns to `false` in case of malfunction, e.g. if there's a problem with the local DB or with the RPC connection. -The endpoint `/nextliquidations` returns a list accounts likely up for liquidation next. The timeframe for that preview defaults to 1h. You can set a custom timeframe by setting an url parameter `timeframe`. Supported units are m (minutes), h (hours), d(days), w(weeks), M(months), y(years). Example: `/nextliquidations?timeframe=3d` - -Using the json parsing tool `jq`, you can pretty-print the output of the metris endpoint and also run queries on it. -There's also a convenience script available with a few potentially useful queries, see `scripts/query-metrics.sh --help`. -(Note: this script doesn't use the ENV vars related to metrics from the .env file - you may need to set the HOST env var). - -You can also set up notifications to Slack or Telegram. Events triggering a notification include -* sentinel restarts -* transactions held back to due the configured gas price limit -* PIC changes relevant for your instance -* insufficient funds for doing liquidations - -In order to set up notifications, see `.env-example` for the relevant configuration items. - -The notification system is modular. If you want support for more channels, consider adding it. See `src/services/slackNotifier.js` for a blueprint. PRs are welcome! - -Sentinel instances also periodically (default: every 12 hours) report basic metrics to a telemetry endpoint. -This helps understanding how many instances are active and what their approximate configuration is. -Reported metrics: -* uuid (randomly generated on first start and preserved in a file "data/uuid.txt") -* chain i -* nodejs version -* sentinel version -* healthy flag (false e.g. if the configured RPC is drifting) -* nr of rpc requests (since last restart) -* account balance (rounded to 3 decimal places) -* memory used by the process - #### Run multiple instances In order to run sentinels for multiple networks in parallel, create network specific env files which are -named `.env-`. -E.g. if you want to run a sentinel for xdai and a sentinel for polygon, prepare env files `.env-xdai` and `.env-polygon` -with the respective settings. -You need to set `DB_PATH` to different values instead of using the default value. -You may also want to set the variable `CHAIN_ID` (e.g. `CHAIN_ID=100` for xdai). This makes sure you don't accidentally +named `.env-`. +E.g. if you want to run a sentinel for Gnosis Chain and a sentinel for polygon, you can prepare env files `.env-gnosis` and `.env-polygon` +with the respective settings. +You need to set `DB_PATH` to different values instead of using the default value, otherwise the instances will conflict. +You may also want to set the variable `CHAIN_ID` (e.g. `CHAIN_ID=100` for gnosis). This makes sure you don't accidentally use the wrong RPC node or write to a sqlite file created for a different network. With the env files in place, you can start instances like this: @@ -126,15 +145,19 @@ With the env files in place, you can start instances like this: yarn start ``` -For example: `yarn start xdai`will start an instance configured according to the settings in `.env-xdai`. +For example: `yarn start gnosis`will start an instance configured according to the settings in `.env-gnosis`. -If you use systemd, create instance specific copies of the service file, e.g. `superfluid-sentinel-xdai.service`, and +If you use systemd, create instance specific copies of the service file, e.g. `superfluid-sentinel-gnosis.service`, and add the network name to the start command, e.g. `ExecStart=/home/ubuntu/.nvm/nvm-exec yarn start xdai`. +Instead of duplicating service definitions, you may instead also use [instantiated units](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/using_systemd_unit_files_to_customize_and_optimize_your_system/assembly_working-with-systemd-unit-files_working-with-systemd#con_working-with-instantiated-units_assembly_working-with-systemd-unit-files) to keep the setup simple with multiple sentinel instances. Such a template systemd unit file could be named `superfluid-sentinel-@.service` and contain this execution command: +``` +ExecStart= start %i +``` #### Update -In order to update to a new version, first go to https://github.com/superfluid-finance/superfluid-sentinel/releases in order to see recent releases and a brief description. -New releases may also add or change default configuration items, documented in this README and `.env-example`. +In order to update to a new version, first go to https://github.com/superfluid-finance/superfluid-sentinel/releases in order to see recent releases and a brief description. +New releases may also add or change default configuration items, documented in this README and `.env.example`. Once you decided to do an update to the latest version, cd into the sentinel directory and do ``` @@ -152,67 +175,69 @@ systemctl restart superfluid-sentinel.service Finally, you may check the logs in order to make sure the restart went well. -### Docker Setup - -This part of the guide assumes you have a recent version of Docker and docker-compose installed. -You also need to have an `.env` file with the wanted configuration in the project root directory. +## Advanced configuration -If running with Docker, you can choose between a minimal and a default configuration. -The default configuration `docker-compose.yml` runs the sentinel together with [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/) for monitoring. -The minimal configuration `docker-compose-minimal.yml` runs only the sentinel service. Choose this if you have a resource constrained environment and/or have your own monitoring solution. -You can switch to the minimal configuration by setting `COMPOSE_FILE=docker-compose-minimal.yml` in your `.env` file. - -In order to start the container(s): +### Flowrate Thresholds +In order to exclude _dust streams_ from liquidations, you can provide a file `thresholds.json` declaring a list of such thresholds. +An example is provided in `thresholds.json.example`: ``` -docker-compose up +{ + "schema-version": "1", + "name": "sentinel-threshold", + "networks": { + "137": { + "name": "Polygon", + "thresholds": [{ + "address": "0xCAa7349CEA390F89641fe306D93591f87595dc1F", + "above": "3858024691" + }] + } + } +} ``` -On first invocation, this builds a Docker image for the sentinel from source (which can take a while) and then starts it. -On consecutive runs, the image is reused. -The sqlite DB is stored in a Docker volume named `superfluid-sentinel_data` (can differ based on the name of your local -directory). +* `address` is a Super Token address +* `above` is a flowrate threshold (in wei per second) -In order to run in the background (incl. auto-restart on crash and on reboot), start with +If critical or insolvent streams of a Super Token with a flowrate belove that value are found, they are ignored. +This mechanism can be used to avoid spending tx fees for dust streams with negligible buffer amounts compensating for the liquidation tx fees. -``` -docker-compose up -d -``` +Note that dust streams don't intrinsically come with negligible buffer. The protocol allows to set a minimum buffer amount to be paid. +There is however existing streams which were created before such a minimum buffer was put in place. -Use `docker-compose logs` in order to see the logs in this case (add `-f` to follow the live log). +## Monitoring, Alerting & Telemetry -If you're running the default configuration, you can now access a Grafana dashboard at the configured port (default: 3000). -The initial credentials are admin:admin, you will be asked to change the password on first login. -A sentinel specific dashboard is not yet included, but you can already select a generic dashboard for node.js applications. -Work is in progress for adding more sentinel specific metrics to the prometheus exporter of the sentinel application which feeds Grafana. +The sentinel can provide monitoring information. In the default configuration, this is available on port 9100 and returns json formatted data. -If you need to or want to rebuild the sentinel database from scratch, delete the volume: -First, destroy the container with `docker-compose rm`. -Then delete the volume with `docker volume rm superfluid-sentinel_data` (adapt the name if it differs on your system). +The endpoint `/` returns a status summary, including a flag `healthy` which turns to `false` in case of malfunction, e.g. if there's a problem with the local DB or with the RPC connection. +The endpoint `/nextliquidations` returns a list accounts likely up for liquidation next. The timeframe for that preview defaults to 1h. You can set a custom timeframe by setting an url parameter `timeframe`. Supported units are m (minutes), h (hours), d(days), w(weeks), M(months), y(years). Example: `/nextliquidations?timeframe=3d` -### Update +Using the json parsing tool `jq`, you can pretty-print the output of the metris endpoint and also run queries on it. +There's also a convenience script available with a few potentially useful queries, see `scripts/query-metrics.sh --help`. +(Note: this script doesn't use the ENV vars related to metrics from the .env file - you need to set the HOST env var if your sentinel isn't listening at http://localhost:9100). -The process for updating docker based sentinel instances will be simplified soon. +You can also set up notifications to Slack or Telegram. Events triggering a notification include +* sentinel restarts +* transactions held back to due the configured gas price limit +* PIC changes relevant for your instance +* insufficient funds for doing liquidations -Currently, after cd'ing into the sentinel directory, you need to first stop with -``` -docker-compose down -``` -Then get the latest code with -``` -git pull -``` -Then remove the sentinel docker container and image: -``` -docker rm superfluid-sentinel_sentinel_1 && docker image rm superfluid-sentinel_sentinel -``` -(the container and image name may differ if your directory is named differently) +In order to set up notifications, see `.env.example` for the relevant configuration items. -Now you can trigger a re-building of the sentinel image with -``` -docker-compose up -``` -After building the image, this will also create a new container based on it and start it. +The notification system is modular. If you want support for more channels, consider adding it. See `src/services/slackNotifier.js` for a blueprint. PRs are welcome! + +Sentinel instances also periodically (default: every 12 hours) report basic metrics to a telemetry endpoint. +This helps understanding how many instances are active and what their approximate configuration is. +Reported metrics: +* uuid (randomly generated on first start and preserved in a file "data/uuid.txt") +* chain i +* nodejs version +* sentinel version +* healthy flag (false e.g. if the configured RPC is drifting) +* nr of rpc requests (since last restart) +* account balance (rounded to 3 decimal places) +* memory used by the process ## Control flow @@ -220,7 +245,7 @@ At startup, the sentinel syncs its state (persisted in the local sqlite DB) by i relevant log events emitted by Superfluid contracts. Based on that state data it then executes the following steps in an endless loop: -1. Load `FlowUpdated` events +1. Load relevant agreement events like `FlowUpdated` 2. Get all SuperToken addresses @@ -256,10 +281,6 @@ When the sentinel submits a transaction, it starts a timeout clock. When the tim transaction (same nonce) with a higher gas price. The starting gas price is currently determined using the `eth_gasPrice` RPC method (updated per transaction). May change in the future. -## Code structure - -[TODO] - ## License [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) From 2c8ad96616cc012a9babe2c7158df55c8f20bc96 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 18:22:10 +0100 Subject: [PATCH 09/10] fix toga script --- scripts/printTOGAstatus.js | 42 +++++++++++++++----------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/scripts/printTOGAstatus.js b/scripts/printTOGAstatus.js index fcaf93a5..e52226d4 100644 --- a/scripts/printTOGAstatus.js +++ b/scripts/printTOGAstatus.js @@ -4,24 +4,11 @@ */ require("dotenv").config(); -const togaABI = require("../src/inc/TOGA.json"); -const Web3 = require("web3"); +const togaArtifact = require("@superfluid-finance/ethereum-contracts/build/truffle/TOGA.json"); +const { Web3 } = require("web3"); const axios = require("axios"); -const { wad4human, toBN } = require("@decentral.ee/web3-helpers"); - -/* CONFIGS */ -const NETWORKS = { - 100: { - name: "xdai", - theGraphQueryUrl: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-xdai", - toga: "0xb7DE52F4281a7a276E18C40F94cd93159C4A2d22" - }, - 137: { - name: "matic", - theGraphQueryUrl: "https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-matic", - toga: "0x6AEAeE5Fd4D05A741723D752D30EE4D72690A8f7" - } -}; +const { wad4human } = require("@decentral.ee/web3-helpers"); +const metadata = require("@superfluid-finance/metadata"); async function getSuperTokens (graphAPI) { const query = `query MyQuery { @@ -45,16 +32,21 @@ async function getSuperTokens (graphAPI) { (async () => { const web3 = new Web3(process.env.HTTP_RPC_NODE); - const chainId = await web3.eth.getChainId(); - const networkConfig = NETWORKS[chainId]; - if (networkConfig === undefined) { - console.error(`no config found for chainId ${chainId}`); + const chainId = parseInt(await web3.eth.getChainId()); + const network = metadata.getNetworkByChainId(chainId); + if (network === undefined) { + console.error(`no network config found for chainId ${chainId}`); + process.exit(1); + } + if (network.contractsV1.toga === undefined) { + console.error(`no TOGA contract in metadata for chainId ${chainId}`); process.exit(1); } - const toga = new web3.eth.Contract(togaABI, networkConfig.toga); + const toga = new web3.eth.Contract(togaArtifact.abi, network.contractsV1.toga); + const graphApiUrl = `https://${network.name}.subgraph.x.superfluid.dev`; const table = []; - const superTokens = await getSuperTokens(networkConfig.theGraphQueryUrl); + const superTokens = await getSuperTokens(graphApiUrl); for (let i = 0; i < superTokens.length; i++) { try { @@ -65,7 +57,7 @@ async function getSuperTokens (graphAPI) { symbol: superTokens[i].symbol, PIC: picInfo.pic, Bond: wad4human(picInfo.bond), - ExitRatePerDay: wad4human(toBN(picInfo.exitRate).mul(toBN(3600 * 24))) + ExitRatePerDay: wad4human(picInfo.exitRate * 86400n) }); } else { console.log(`skipping ${superTokens[i].symbol} (no PIC set, zero bond)`); @@ -74,6 +66,6 @@ async function getSuperTokens (graphAPI) { console.error(err); } } - console.log(`Super Tokens on ${networkConfig.name} with a PIC set and/or a non-zero bond:`); + console.log(`Super Tokens on ${network.name} with a PIC set and/or a non-zero bond:`); console.table(table, ["name", "symbol", "PIC", "Bond", "ExitRatePerDay"]); })(); From 2799ab3a8e6cde40257c5392ff483f20aca3d337 Mon Sep 17 00:00:00 2001 From: didi Date: Tue, 20 Feb 2024 18:46:14 +0100 Subject: [PATCH 10/10] change default DB path --- .env.example | 2 +- docker-compose-with-monitoring.yml | 1 - docker-compose.yml | 1 - src/config/configuration.js | 4 ++-- src/config/loadCmdArgs.js | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index b3a440e7..4b010937 100644 --- a/.env.example +++ b/.env.example @@ -150,7 +150,7 @@ HTTP_RPC_NODE= ## Location of the sqlite database. The file (and non-existing directories in the path) will be created if not existing. ## Note: this is ignored (overridden) when running with Docker. -#DB_PATH=db.sqlite +#DB_PATH=data/db.sqlite ## --- DOCKER PARAMETERS --- diff --git a/docker-compose-with-monitoring.yml b/docker-compose-with-monitoring.yml index e87e6d3f..5cebcac3 100644 --- a/docker-compose-with-monitoring.yml +++ b/docker-compose-with-monitoring.yml @@ -11,7 +11,6 @@ services: env_file: .env environment: - NODE_ENV=production - - DB_PATH=data/db.sqlite # hardcode the port inside the container - METRICS_PORT=9100 ports: diff --git a/docker-compose.yml b/docker-compose.yml index da5e011a..0d309434 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,6 @@ services: env_file: .env environment: - NODE_ENV=production - - DB_PATH=data/db.sqlite # hardcode the port inside the container - METRICS_PORT=9100 ports: diff --git a/src/config/configuration.js b/src/config/configuration.js index 1c7de76a..6889f2a0 100644 --- a/src/config/configuration.js +++ b/src/config/configuration.js @@ -41,7 +41,7 @@ class Config { this.MAX_QUERY_BLOCK_RANGE = config.max_query_block_range || 2000; this.TOKENS = config.TOKENS?.split(","); this.EXCLUDED_TOKENS = config.EXCLUDED_TOKENS?.split(","); - this.DB = (config.db_path !== undefined && config.db_path !== "") ? config.db_path : "./db.sqlite"; + this.DB = (config.db_path !== undefined && config.db_path !== "") ? config.db_path : "data/db.sqlite"; this.ADDITIONAL_LIQUIDATION_DELAY = config.additional_liquidation_delay || 0; this.TX_TIMEOUT = config.tx_timeout * 1000 || 60000; this.PROTOCOL_RELEASE_VERSION = config.protocol_release_version || "v1"; @@ -80,7 +80,7 @@ class Config { this.TOKENS = undefined; this.TOKENS = process.env.TOKENS?.split(","); this.EXCLUDED_TOKENS = process.env.EXCLUDED_TOKENS?.split(","); - this.DB = (process.env.DB_PATH !== undefined && process.env.DB_PATH !== "") ? process.env.DB_PATH : "./db.sqlite"; + this.DB = (process.env.DB_PATH !== undefined && process.env.DB_PATH !== "") ? process.env.DB_PATH : "data/db.sqlite"; this.ADDITIONAL_LIQUIDATION_DELAY = process.env.ADDITIONAL_LIQUIDATION_DELAY || 0; this.TX_TIMEOUT = process.env.TX_TIMEOUT * 1000 || 60000; this.PROTOCOL_RELEASE_VERSION = process.env.PROTOCOL_RELEASE_VERSION || "v1"; diff --git a/src/config/loadCmdArgs.js b/src/config/loadCmdArgs.js index dd0a72fa..25f8106c 100644 --- a/src/config/loadCmdArgs.js +++ b/src/config/loadCmdArgs.js @@ -17,7 +17,7 @@ program .option("--max-query-block-range [value]", "Max query block range (default: 2000)") .option("-t, --tokens [value]", "Addresses of SuperTokens the sentinel should watch (default: all SuperTokens)") .option("-e, --exclude-tokens [value]", "Addresses of SuperTokens the sentinel should excluded (default: none)") - .option("-p, --db-path [value]", "Path of the DB file (default: db.sqlite)") + .option("-p, --db-path [value]", "Path of the DB file (default: data/db.sqlite)") .option("-d, --additional-liquidation-delay [value]", "Time to wait (seconds) after an agreement becoming critical before doing a liquidation (default: 0)") .option("--tx-timeout [value]", "Time to wait (seconds) before re-broadcasting a pending transaction with higher gas price (default: 60)") .option("--protocol-release-version [value]", "Superfluid Protocol Release Version (default: v1)")