From 10dae8d2ed2bed790eb15436eeb8421c7f3e20b2 Mon Sep 17 00:00:00 2001 From: Alberto Monterroso <14013679+Albermonte@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:50:30 +0200 Subject: [PATCH 1/4] feat(scores): add hybrid v2 score rollout Add marker-gated activity synchronization and independent v1/v2 score persistence. Expose score-version selection across the API and dashboard with freshness states, recovery logging, migrations, tests, and rollout documentation. --- .env.example | 7 + MIGRATION.md | 71 +- README.md | 63 +- app/app.vue | 144 ++- app/components/ScorePie.vue | 6 +- app/components/ScorePies.vue | 95 +- app/components/ScoreVersionSelect.vue | 35 + app/components/ValidatorsTable.vue | 49 +- app/components/validator/LayoutDashboard.vue | 15 +- app/components/validator/LayoutDeepDive.vue | 41 +- app/components/validator/LayoutProfile.vue | 11 +- app/composables/useScoreVersionQuery.ts | 30 + app/composables/useValidatorCharts.test.ts | 78 +- app/composables/useValidatorCharts.ts | 144 ++- app/pages/index.vue | 37 +- app/pages/validator/[address].vue | 5 +- app/utils/score-display.test.ts | 65 + app/utils/score-display.ts | 48 + app/utils/score-version.test.ts | 41 + app/utils/score-version.ts | 32 + ...-activity-integrity-hybrid-score-design.md | 280 +++++ nuxt.config.test.ts | 28 + nuxt.config.ts | 20 +- package.json | 8 +- .../build.config.ts | 1 + .../nimiq-validator-trustscore/package.json | 4 + .../src/fetcher.test.ts | 177 +++ .../nimiq-validator-trustscore/src/fetcher.ts | 116 +- .../src/score-v2.test.ts | 269 ++++ .../src/score-v2.ts | 255 ++++ .../nimiq-validator-trustscore/src/types.ts | 33 + server/api/[version]/status.get.test.ts | 131 ++ server/api/[version]/status.get.ts | 108 +- .../api/[version]/validators/[address].get.ts | 35 +- server/api/[version]/validators/index.get.ts | 30 +- ..._validator_activity_integrity_score_v2.sql | 55 + server/db/migrations/meta/0003_snapshot.json | 405 ++++++ server/db/migrations/meta/0004_snapshot.json | 412 ++++++ server/db/migrations/meta/0005_snapshot.json | 578 +++++++++ server/db/migrations/meta/_journal.json | 21 + .../migrations/migrations.integration.test.ts | 126 ++ server/db/schema.ts | 61 +- server/tasks/cron/sync.test.ts | 114 ++ server/tasks/cron/sync.ts | 30 +- server/tasks/sync/epochs.test.ts | 166 +++ server/tasks/sync/epochs.ts | 143 ++- server/tasks/sync/scores.test.ts | 114 ++ server/tasks/sync/scores.ts | 57 + server/tasks/sync/snapshot.test.ts | 97 ++ server/tasks/sync/snapshot.ts | 24 +- server/test/db-harness.ts | 159 +++ server/utils/activities.ts | 325 ++++- server/utils/activity-completeness.test.ts | 38 - server/utils/activity-completeness.ts | 17 - server/utils/activity-epochs.test.ts | 331 +++++ server/utils/activity-epochs.ts | 499 ++++++++ server/utils/activity-integrity.test.ts | 83 ++ server/utils/activity-integrity.ts | 43 + .../activity-persistence.integration.test.ts | 1108 +++++++++++++++++ server/utils/activity-sync.test.ts | 194 +++ server/utils/activity-sync.ts | 144 +++ server/utils/cron-task-runner.test.ts | 26 + server/utils/cron-task-runner.ts | 57 + server/utils/drizzle.ts | 5 +- server/utils/schemas.ts | 2 + server/utils/score-api.test.ts | 276 ++++ server/utils/score-api.ts | 183 +++ server/utils/scores.integration.test.ts | 647 ++++++++++ server/utils/scores.test.ts | 160 +++ server/utils/scores.ts | 825 ++++++++++-- server/utils/slack.ts | 2 +- server/utils/types.ts | 25 +- server/utils/validators.integration.test.ts | 143 +++ server/utils/validators.ts | 113 +- vitest.config.ts | 5 + wrangler.json | 8 +- 76 files changed, 9854 insertions(+), 479 deletions(-) create mode 100644 app/components/ScoreVersionSelect.vue create mode 100644 app/composables/useScoreVersionQuery.ts create mode 100644 app/utils/score-display.test.ts create mode 100644 app/utils/score-display.ts create mode 100644 app/utils/score-version.test.ts create mode 100644 app/utils/score-version.ts create mode 100644 docs/superpowers/specs/2026-07-28-validator-activity-integrity-hybrid-score-design.md create mode 100644 nuxt.config.test.ts create mode 100644 packages/nimiq-validator-trustscore/src/fetcher.test.ts create mode 100644 packages/nimiq-validator-trustscore/src/score-v2.test.ts create mode 100644 packages/nimiq-validator-trustscore/src/score-v2.ts create mode 100644 server/api/[version]/status.get.test.ts create mode 100644 server/db/migrations/0005_validator_activity_integrity_score_v2.sql create mode 100644 server/db/migrations/meta/0003_snapshot.json create mode 100644 server/db/migrations/meta/0004_snapshot.json create mode 100644 server/db/migrations/meta/0005_snapshot.json create mode 100644 server/db/migrations/migrations.integration.test.ts create mode 100644 server/tasks/cron/sync.test.ts create mode 100644 server/tasks/sync/epochs.test.ts create mode 100644 server/tasks/sync/scores.test.ts create mode 100644 server/tasks/sync/scores.ts create mode 100644 server/tasks/sync/snapshot.test.ts create mode 100644 server/test/db-harness.ts delete mode 100644 server/utils/activity-completeness.test.ts delete mode 100644 server/utils/activity-completeness.ts create mode 100644 server/utils/activity-epochs.test.ts create mode 100644 server/utils/activity-epochs.ts create mode 100644 server/utils/activity-integrity.test.ts create mode 100644 server/utils/activity-integrity.ts create mode 100644 server/utils/activity-persistence.integration.test.ts create mode 100644 server/utils/activity-sync.test.ts create mode 100644 server/utils/activity-sync.ts create mode 100644 server/utils/cron-task-runner.test.ts create mode 100644 server/utils/cron-task-runner.ts create mode 100644 server/utils/score-api.test.ts create mode 100644 server/utils/score-api.ts create mode 100644 server/utils/scores.integration.test.ts create mode 100644 server/utils/scores.test.ts create mode 100644 server/utils/validators.integration.test.ts diff --git a/.env.example b/.env.example index 0e0e752d..3239b593 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,10 @@ NUXT_PUBLIC_NIMIQ_NETWORK=test-albatross ALBATROSS_RPC_NODE_URL= +# off: v1 only; shadow: write v1/v2, serve v1; active: write v1/v2, serve v2 +NUXT_SCORE_V2_MODE=off + +# Migration-only NuxtHub D1 HTTP credentials. Keep out of deployed runtime vars. +NUXT_HUB_CLOUDFLARE_ACCOUNT_ID= +NUXT_HUB_CLOUDFLARE_DATABASE_ID= +NUXT_HUB_CLOUDFLARE_API_TOKEN= diff --git a/MIGRATION.md b/MIGRATION.md index 00546a97..f996e094 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2,7 +2,7 @@ ## Background -Cloudflare Pages does NOT support scheduled tasks (cron jobs). This project requires hourly syncing, so we migrated to Cloudflare Workers. +Cloudflare Pages does not support scheduled tasks. This project requires six-hour syncing, so it uses Cloudflare Workers. ## Setting Up Redirects @@ -60,3 +60,72 @@ curl -I https://validators-api-testnet.pages.dev/api/v1/status 2. Deploy redirects to Pages projects 3. Monitor for 1-2 weeks 4. (Optional) Deprecate legacy `.pages.dev` URLs and add a custom domain + +## Activity integrity and score v2 rollout + +This implementation does not perform any remote migration or deployment. Run each remote command manually, testnet first, after reviewing its target. + +### Safeguards + +1. Authenticate Wrangler and confirm the expected account: + + ```bash + pnpm exec wrangler login + pnpm exec wrangler whoami + ``` + +2. Inspect applied migrations and relevant table/index definitions. Never assume a migration is already applied: + + ```bash + pnpm exec wrangler d1 execute validators-api-testnet --remote --env testnet --command "SELECT id, name, applied_at FROM _hub_migrations ORDER BY id;" + pnpm exec wrangler d1 execute validators-api-testnet --remote --env testnet --command "SELECT name, type, sql FROM sqlite_schema WHERE name IN ('_hub_migrations', 'activities', 'activity_epochs', 'scores', 'validators') OR name LIKE 'idx_%' ORDER BY type, name;" + ``` + +3. Export a backup outside this repository: + + ```bash + pnpm exec wrangler d1 export validators-api-testnet --remote --env testnet --output ../validators-api-testnet-before-score-v2.sql + ``` + +4. Put migration-only D1 HTTP credentials in `.env.testnet`: + + ```dotenv + NUXT_HUB_CLOUDFLARE_ACCOUNT_ID=... + NUXT_HUB_CLOUDFLARE_DATABASE_ID=... + NUXT_HUB_CLOUDFLARE_API_TOKEN=... + ``` + + Keep these values out of deployed runtime variables. Migration scripts enable the HTTP driver only for their own command and fail if any credential is missing. + +Repeat inspection and backup with `validators-api-mainnet` and without `--env testnet` only after testnet validation passes. + +### Testnet-first sequence + +1. Keep testnet `NUXT_SCORE_V2_MODE=off`. +2. Apply all pending tracked migrations: + + ```bash + pnpm db:migrate:testnet + ``` + + This uses NuxtHub's basename-compatible `_hub_migrations` ledger. Do not substitute `wrangler d1 migrations apply`; Wrangler records full filenames and can replay an existing NuxtHub baseline. + +3. Inspect `_hub_migrations` and relevant schemas again. +4. Deploy testnet while v2 remains off. +5. Let the six-hour job discover epochs, repair recent activity, store the snapshot, then calculate v1 scores. +6. Set `NUXT_SCORE_V2_MODE=shadow`, deploy, and validate v1/v2 rows, activity coverage, score versions, and `current`, `stale`, or `no_score` API states. +7. Set `NUXT_SCORE_V2_MODE=active` only after shadow results pass. +8. Repeat the same inspect, backup, migrate, off, repair, shadow, validate, and active sequence for mainnet. + +Activity marker interpretation during validation: + +- `syncing`: repair attempt is inside its six-hour lease. +- `complete`: stored `finalized` marker has an exact elected set and matching counts. +- `incomplete`: expected activity has no valid finalized marker. +- `failed`: attempt rolled back and recorded an error for retry. + +API list, detail, and status endpoints accept `score-version=1|2`. Omitted version follows rollout mode: `off` and `shadow` select v1; `active` selects v2. Selected-version rows are filtered before latest-score selection. `current` means latest completed epoch is covered, `stale` preserves an older valid score, and `no_score` returns null score values. + +### Rollback + +Set `NUXT_SCORE_V2_MODE=off`, deploy the configuration change, and request `score-version=1` explicitly while caches settle. Keep both v1 and v2 rows intact. Do not roll back schema or score data destructively. diff --git a/README.md b/README.md index 93d47c8e..a3421053 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,17 @@ The Validators API provides endpoints to retrieve validator information for inte | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | [/api/v1/validators](https://validators-api-main.je-cf9.workers.dev/api/v1/validators) | Retrieves the validator list. See [query params](./server/utils/schemas.ts#L54) | | [/api/v1/validators/:validator_address](https://validators-api-main.je-cf9.workers.dev/api/v1/validators/NQ98%20D3KE%208EQ8%20Y7DK%20G1MT%203P5T%202PHX%2018V5%20UEC1) | Retrieves the validator information | +| [/api/v1/status](https://validators-api-main.je-cf9.workers.dev/api/v1/status) | Retrieves activity coverage and selected score status | | [/api/v1/supply](https://validators-api-main.je-cf9.workers.dev/api/v1/supply) | Retrieves supply status | +Validator list, detail, and status endpoints accept `score-version=1|2`. Without it, `NUXT_SCORE_V2_MODE=off` and `shadow` select v1, while `active` selects v2. Queries filter by the selected version before choosing the latest score, so responses never mix v1 and v2 history. + +Every score includes its version and data state: + +- `current`: selected-version score covers the latest completed epoch. +- `stale`: an older valid score remains available while current activity or scoring is incomplete. +- `no_score`: no selected-version score exists; numeric fields remain `null`, not zero. + ## Validators Dashboard The Validators Dashboard is a simple Nuxt application that displays all validators along with their scores. You can access the dashboard here: https://validators-api-main.je-cf9.workers.dev/ @@ -138,7 +147,18 @@ We also do have an UI component to visualize the range, check the status, and de ### Fetcher -The fetcher is a process that retrieves data from the Nimiq network and stores it in a D1 database. The fetcher runs every hour and collects data about the validators in two different ways: +The fetcher retrieves data from the Nimiq network and stores it in D1. It runs every six hours in this order: + +1. Discover completed epochs and verify or repair activity snapshots. +2. Store the current validator snapshot. +3. Calculate scores from finalized activity markers. + +Completed-epoch activity uses marker-backed integrity checks. Operator-facing marker states are: + +- `syncing`: repair attempt owns a fresh six-hour lease. +- `complete`: stored `finalized` marker has matching election-set hash and elected counts. +- `incomplete`: expected epoch has no finalized marker or exact stored set. +- `failed`: attempt rolled back and retained its error for retry. #### Ended epochs @@ -213,44 +233,35 @@ Where `env`: `testnet` (omit `-e env` for mainnet production). ### D1 Migrations -When adding a new SQL migration under `server/db/migrations/`, apply it to the remote D1 database. +Never assume remote migration state. Authenticate Wrangler, inspect `_hub_migrations` and relevant schemas, and export a backup outside the repository before applying migrations. Set the migration-only `NUXT_HUB_CLOUDFLARE_ACCOUNT_ID`, `NUXT_HUB_CLOUDFLARE_DATABASE_ID`, and `NUXT_HUB_CLOUDFLARE_API_TOKEN` values in the target `.env` file. -For the `cron_runs` table: +Generic migration commands use NuxtHub's `_hub_migrations` basenames and apply all pending files under `server/db/migrations/`: ```bash -pnpm db:apply:cron-runs:mainnet +pnpm db:migrate:testnet +pnpm db:migrate:mainnet ``` -Testnet: - -```bash -pnpm db:apply:cron-runs:testnet -``` - -Required schema: - -- `validators.is_listed` must exist in all remote D1 databases. +Always migrate and validate testnet first. See [MIGRATION.md](./MIGRATION.md) for inspection, backup, rollout, and rollback steps. -If the column is missing, apply it manually: +This implementation does not run any remote migration or deployment. -Mainnet: +**Environments** (configured in `wrangler.json`): -```bash -pnpm db:apply:is-listed:mainnet -``` +- `production`: [Validators API Mainnet](https://validators-api-main.je-cf9.workers.dev) via manual `wrangler deploy` +- `testnet`: [Validators API Testnet](https://validators-api-test.je-cf9.workers.dev) via manual `wrangler deploy --env testnet` -Testnet: +Each environment has its own D1 database, KV cache, and R2 blob. Sync runs every six hours via Cloudflare cron triggers (see `server/tasks/sync/`). -```bash -pnpm db:apply:is-listed:testnet -``` +### Score v2 rollout -**Environments** (configured in `wrangler.json`): +Set `NUXT_SCORE_V2_MODE` per environment: -- `production`: [Validators API Mainnet](https://validators-api-main.je-cf9.workers.dev) via manual `wrangler deploy` -- `testnet`: [Validators API Testnet](https://validators-api-test.je-cf9.workers.dev) via manual `wrangler deploy --env testnet` +- `off`: write and serve v1 only. +- `shadow`: keep v1 writes, also write v2, and serve v1 by default. +- `active`: keep v1 and v2 writes, and serve v2 by default. -Each environment has its own D1 database, KV cache, and R2 blob. Sync runs every 12 hours via Cloudflare cron triggers (see `server/tasks/sync/`). +Rollback requires no destructive schema or data change: set `NUXT_SCORE_V2_MODE=off`, keep all v1/v2 rows intact, and request `score-version=1` explicitly while the configuration change propagates. ### Deployment Migration diff --git a/app/app.vue b/app/app.vue index f4638cd7..f6ccac13 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,17 +1,93 @@ diff --git a/app/components/ScoreVersionSelect.vue b/app/components/ScoreVersionSelect.vue new file mode 100644 index 00000000..e19f9e85 --- /dev/null +++ b/app/components/ScoreVersionSelect.vue @@ -0,0 +1,35 @@ + + + diff --git a/app/components/ValidatorsTable.vue b/app/components/ValidatorsTable.vue index ca11aef3..0c078bed 100644 --- a/app/components/ValidatorsTable.vue +++ b/app/components/ValidatorsTable.vue @@ -1,11 +1,17 @@