From 37d7274785f8389b0aa7d6b0f18db438f9cdb17b Mon Sep 17 00:00:00 2001 From: onmax Date: Sat, 14 Feb 2026 11:39:21 +0100 Subject: [PATCH 1/6] chore: add Pages redirects from nuxt.dev API to Workers --- public/_redirects | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 public/_redirects diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..0485af8 --- /dev/null +++ b/public/_redirects @@ -0,0 +1,2 @@ +/api/* https://validators-api-test.je-cf9.workers.dev/api/:splat 308 +/api https://validators-api-test.je-cf9.workers.dev/api 308 From 43cae2489f84acd2828972347bcbf23f4a57d8ac Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 25 Feb 2026 21:17:14 +0100 Subject: [PATCH 2/6] fix(status): allow one-epoch score lag --- README.md | 2 +- app/app.vue | 2 +- server/api/[version]/status.get.ts | 12 ++++++- server/utils/score-freshness.test.ts | 24 ++++++++++++++ server/utils/score-freshness.ts | 19 +++++++++++ server/utils/scores.ts | 48 ++++++++++++++++++++++++---- 6 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 server/utils/score-freshness.test.ts create mode 100644 server/utils/score-freshness.ts diff --git a/README.md b/README.md index 383d8c7..214b34f 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,6 @@ pnpm db:apply:cron-runs:testnet | `testnet` | [Validators API Testnet](https://validators-api-testnet.pages.dev) | Push to `main` | | `testnet-preview` | [Validators API Testnet Preview](https://dev.validators-api-testnet.pages.dev) | Push to any branch | -Each environment has its own D1 database, KV cache, and R2 blob. Sync runs hourly via Cloudflare cron triggers (see `server/tasks/sync/`). +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/`). **Write operations to `main` are restricted**, only via PR. diff --git a/app/app.vue b/app/app.vue index ee7fcbc..0abff0a 100644 --- a/app/app.vue +++ b/app/app.vue @@ -124,7 +124,7 @@ const currentEnvItem = { branch: gitBranch, network: nimiqNetwork, link: environ

- Note: Data synchronization is handled automatically by scheduled tasks that run hourly. Please wait for the next sync cycle or contact an administrator if the issue persists. + Note: Data synchronization is handled automatically by scheduled tasks that run every 12 hours. A score lag of up to 1 epoch can be expected between sync cycles.

diff --git a/server/api/[version]/status.get.ts b/server/api/[version]/status.get.ts index c60e49c..c55c154 100644 --- a/server/api/[version]/status.get.ts +++ b/server/api/[version]/status.get.ts @@ -43,14 +43,24 @@ export default defineCachedEventHandler(async () => { if (!headBlockOk) throw createError(errorHeadBlockNumber || 'No head block number') + const allowedScoreLagEpochs = 1 + const latestScoreEpoch = await getLatestScoreEpoch() + const scoreLagEpochs = getScoreLagEpochs({ + toEpoch: range.toEpoch, + latestScoreEpoch, + }) + const missingEpochs = await findMissingEpochs(range) - const missingScore = await isMissingScore(range) + const missingScore = await isScoreMissingWithLag(range, allowedScoreLagEpochs, latestScoreEpoch) return { range, validators: validatorsEpoch, missingEpochs, missingScore, + latestScoreEpoch, + scoreLagEpochs, + allowedScoreLagEpochs, blockchain: { network, headBlockNumber }, } }) diff --git a/server/utils/score-freshness.test.ts b/server/utils/score-freshness.test.ts new file mode 100644 index 0000000..ee5e71b --- /dev/null +++ b/server/utils/score-freshness.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' +import { getScoreLagEpochs, isScoreLagMissing } from './score-freshness' + +describe('score freshness', () => { + it('considers score synced when latest score epoch equals current toEpoch', () => { + expect(getScoreLagEpochs({ toEpoch: 100, latestScoreEpoch: 100 })).toBe(0) + expect(isScoreLagMissing({ toEpoch: 100, latestScoreEpoch: 100, allowedLagEpochs: 1 })).toBe(false) + }) + + it('considers score synced when lag is exactly one epoch', () => { + expect(getScoreLagEpochs({ toEpoch: 100, latestScoreEpoch: 99 })).toBe(1) + expect(isScoreLagMissing({ toEpoch: 100, latestScoreEpoch: 99, allowedLagEpochs: 1 })).toBe(false) + }) + + it('considers score missing when lag is two epochs', () => { + expect(getScoreLagEpochs({ toEpoch: 100, latestScoreEpoch: 98 })).toBe(2) + expect(isScoreLagMissing({ toEpoch: 100, latestScoreEpoch: 98, allowedLagEpochs: 1 })).toBe(true) + }) + + it('considers score missing when no score exists', () => { + expect(getScoreLagEpochs({ toEpoch: 100, latestScoreEpoch: null })).toBeNull() + expect(isScoreLagMissing({ toEpoch: 100, latestScoreEpoch: null, allowedLagEpochs: 1 })).toBe(true) + }) +}) diff --git a/server/utils/score-freshness.ts b/server/utils/score-freshness.ts new file mode 100644 index 0000000..c7c5cdb --- /dev/null +++ b/server/utils/score-freshness.ts @@ -0,0 +1,19 @@ +export interface ScoreFreshnessParams { + toEpoch: number + latestScoreEpoch: number | null + allowedLagEpochs?: number +} + +export function getScoreLagEpochs({ toEpoch, latestScoreEpoch }: Pick): number | null { + if (latestScoreEpoch === null) + return null + + return Math.max(0, toEpoch - latestScoreEpoch) +} + +export function isScoreLagMissing({ toEpoch, latestScoreEpoch, allowedLagEpochs = 1 }: ScoreFreshnessParams): boolean { + if (latestScoreEpoch === null) + return true + + return (toEpoch - latestScoreEpoch) > allowedLagEpochs +} diff --git a/server/utils/scores.ts b/server/utils/scores.ts index 3e32a06..77ce0da 100644 --- a/server/utils/scores.ts +++ b/server/utils/scores.ts @@ -1,9 +1,10 @@ import type { Range, Result, ScoreParams } from 'nimiq-validator-trustscore/types' import type { NewScore } from './drizzle' -import { and, count, desc, eq, gte, lte, or } from 'drizzle-orm' +import { and, desc, eq, gte, lte, or, sql } from 'drizzle-orm' import { getRange } from 'nimiq-validator-trustscore/range' import { computeScore } from 'nimiq-validator-trustscore/score' import { activity } from '../db/schema' +import { isScoreLagMissing } from './score-freshness' import { getStoredValidatorsId } from './validators' interface CalculateScoreResult { @@ -117,12 +118,45 @@ export async function upsertScoresSnapshotEpoch(): Result return [true, undefined, { scores, range }] } -export async function isMissingScore(range: Range): Promise { - const scoreCount = await useDrizzle() - .select({ count: count(tables.scores.epochNumber) }) +export async function getLatestScoreEpoch(): Promise { + const latestScoreEpoch = await useDrizzle() + .select({ + latestScoreEpoch: sql`max(${tables.scores.epochNumber})`, + }) .from(tables.scores) - .where(eq(tables.scores.epochNumber, range.toEpoch)) .get() - .then(res => res?.count || 0) - return scoreCount === 0 + .then((res) => { + const value = res?.latestScoreEpoch + if (value === null || value === undefined) + return null + if (typeof value === 'number') + return value + + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null + }) + + return latestScoreEpoch +} + +export async function isScoreMissingWithLag( + range: Range, + allowedLagEpochs = 1, + latestScoreEpoch?: number | null, +): Promise { + const epoch = latestScoreEpoch === undefined ? await getLatestScoreEpoch() : latestScoreEpoch + return isScoreLagMissing({ + toEpoch: range.toEpoch, + latestScoreEpoch: epoch, + allowedLagEpochs, + }) +} + +export async function isMissingScore(range: Range): Promise { + const latestScoreEpoch = await getLatestScoreEpoch() + return isScoreLagMissing({ + toEpoch: range.toEpoch, + latestScoreEpoch, + allowedLagEpochs: 0, + }) } From faae094dcb4c6012228379af47b264fe2dac33c9 Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 25 Feb 2026 21:20:39 +0100 Subject: [PATCH 3/6] refactor(scores): remove unused strict score helper --- server/utils/scores.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server/utils/scores.ts b/server/utils/scores.ts index 77ce0da..ca00e21 100644 --- a/server/utils/scores.ts +++ b/server/utils/scores.ts @@ -151,12 +151,3 @@ export async function isScoreMissingWithLag( allowedLagEpochs, }) } - -export async function isMissingScore(range: Range): Promise { - const latestScoreEpoch = await getLatestScoreEpoch() - return isScoreLagMissing({ - toEpoch: range.toEpoch, - latestScoreEpoch, - allowedLagEpochs: 0, - }) -} From 0b4635cdc847ff063cd159a0597e12d84545b556 Mon Sep 17 00:00:00 2001 From: onmax Date: Wed, 25 Feb 2026 22:14:56 +0100 Subject: [PATCH 4/6] fix(deploy): correct validator assets and redirects --- public/_redirects | 2 -- server/utils/validators-bundle.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 public/_redirects diff --git a/public/_redirects b/public/_redirects deleted file mode 100644 index 0485af8..0000000 --- a/public/_redirects +++ /dev/null @@ -1,2 +0,0 @@ -/api/* https://validators-api-test.je-cf9.workers.dev/api/:splat 308 -/api https://validators-api-test.je-cf9.workers.dev/api 308 diff --git a/server/utils/validators-bundle.ts b/server/utils/validators-bundle.ts index d70d705..6ef4306 100644 --- a/server/utils/validators-bundle.ts +++ b/server/utils/validators-bundle.ts @@ -12,8 +12,8 @@ export async function importValidatorsBundled(nimiqNetwork?: string, options: Im return [false, 'Nimiq network is required', undefined] const { shouldStore = true } = options - const storage = useStorage('assets:server:validators') - const keys = await storage.getKeys(`${nimiqNetwork}`) + const storage = useStorage('assets:public') + const keys = await storage.getKeys(`validators/${nimiqNetwork}`) const validators: ValidatorJSON[] = [] for (const key of keys) { From 8ec2b408093559bce43b7ebe22a73c671b42e96d Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 26 Feb 2026 08:13:24 +0100 Subject: [PATCH 5/6] chore(cf): bump compatibility date --- nuxt.config.ts | 2 +- wrangler.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 5febaf2..7f99f74 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -161,5 +161,5 @@ export default defineNuxtConfig({ }, }, - compatibilityDate: '2025-03-21', + compatibilityDate: '2026-02-26', }) diff --git a/wrangler.json b/wrangler.json index b871557..c134150 100644 --- a/wrangler.json +++ b/wrangler.json @@ -4,7 +4,7 @@ "main": "dist/server/index.mjs", "assets": { "directory": "dist/public" }, "account_id": "cf9baad7d68d7ee717f3339731e81dfb", - "compatibility_date": "2025-01-01", + "compatibility_date": "2026-02-26", "compatibility_flags": ["nodejs_compat"], "observability": { "enabled": true, From 4bc2c2132a9d10a4dd9a4d08bbbb40610b976ccd Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 26 Feb 2026 08:23:02 +0100 Subject: [PATCH 6/6] fix(build): raise node heap for nuxt build --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43bca48..75f5853 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev:testnet:prod": "nr dev:packages && nuxt dev --remote=production --dotenv .env.testnet", "dev:local": "nr dev:packages && nuxt dev --dotenv .env.local", "dev:packages": "nr -C packages -r dev", - "build": "pnpm validators:bundle:generate && nr -r build && nuxt build", + "build": "pnpm validators:bundle:generate && nr -r build && NODE_OPTIONS=--max-old-space-size=4096 nuxt build", "generate": "nuxt generate", "preview": "npx wrangler --cwd .output dev", "postinstall": "nuxt prepare",