Skip to content

Commit a7821a2

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/validator-profile-unlisted-lifecycle
2 parents 5bcd354 + 44247cb commit a7821a2

File tree

9 files changed

+91
-13
lines changed

9 files changed

+91
-13
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ pnpm db:apply:cron-runs:testnet
238238
| `testnet` | [Validators API Testnet](https://validators-api-test.workers.dev) | Manual `wrangler deploy --env testnet` |
239239
| `testnet-preview` | [Validators API Testnet Preview](https://validators-api-test.workers.dev) | Manual deployment |
240240

241-
Each environment has its own D1 database, KV cache, and R2 blob. Sync runs hourly via Cloudflare cron triggers (see `server/tasks/sync/`).
241+
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/`).
242242

243243
### Deployment Migration
244244

app/app.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ const currentEnvItem = { branch: gitBranch, network: nimiqNetwork, link: environ
124124
<hr f-my-sm border-red-600>
125125

126126
<p f-mt-md text="f-sm red-1100/80">
127-
<strong>Note:</strong> 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.
127+
<strong>Note:</strong> 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.
128128
</p>
129129
</div>
130130

nuxt.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,5 @@ export default defineNuxtConfig({
161161
},
162162
},
163163

164-
compatibilityDate: '2025-03-21',
164+
compatibilityDate: '2026-02-26',
165165
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"dev:testnet:prod": "nr dev:packages && nuxt dev --remote=production --dotenv .env.testnet",
1414
"dev:local": "nr dev:packages && nuxt dev --dotenv .env.local",
1515
"dev:packages": "nr -C packages -r dev",
16-
"build": "pnpm validators:bundle:generate && nr -r build && nuxt build",
16+
"build": "pnpm validators:bundle:generate && nr -r build && NODE_OPTIONS=--max-old-space-size=4096 nuxt build",
1717
"generate": "nuxt generate",
1818
"preview": "npx wrangler --cwd .output dev",
1919
"postinstall": "nuxt prepare",

server/api/[version]/status.get.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,24 @@ export default defineCachedEventHandler(async () => {
4444
if (!headBlockOk)
4545
throw createError(errorHeadBlockNumber || 'No head block number')
4646

47+
const allowedScoreLagEpochs = 1
48+
const latestScoreEpoch = await getLatestScoreEpoch()
49+
const scoreLagEpochs = getScoreLagEpochs({
50+
toEpoch: range.toEpoch,
51+
latestScoreEpoch,
52+
})
53+
4754
const missingEpochs = await findMissingEpochs(range)
48-
const missingScore = await isMissingScore(range)
55+
const missingScore = await isScoreMissingWithLag(range, allowedScoreLagEpochs, latestScoreEpoch)
4956

5057
return {
5158
range,
5259
validators: validatorsEpoch,
5360
missingEpochs,
5461
missingScore,
62+
latestScoreEpoch,
63+
scoreLagEpochs,
64+
allowedScoreLagEpochs,
5565
blockchain: { network, headBlockNumber },
5666
}
5767
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { getScoreLagEpochs, isScoreLagMissing } from './score-freshness'
3+
4+
describe('score freshness', () => {
5+
it('considers score synced when latest score epoch equals current toEpoch', () => {
6+
expect(getScoreLagEpochs({ toEpoch: 100, latestScoreEpoch: 100 })).toBe(0)
7+
expect(isScoreLagMissing({ toEpoch: 100, latestScoreEpoch: 100, allowedLagEpochs: 1 })).toBe(false)
8+
})
9+
10+
it('considers score synced when lag is exactly one epoch', () => {
11+
expect(getScoreLagEpochs({ toEpoch: 100, latestScoreEpoch: 99 })).toBe(1)
12+
expect(isScoreLagMissing({ toEpoch: 100, latestScoreEpoch: 99, allowedLagEpochs: 1 })).toBe(false)
13+
})
14+
15+
it('considers score missing when lag is two epochs', () => {
16+
expect(getScoreLagEpochs({ toEpoch: 100, latestScoreEpoch: 98 })).toBe(2)
17+
expect(isScoreLagMissing({ toEpoch: 100, latestScoreEpoch: 98, allowedLagEpochs: 1 })).toBe(true)
18+
})
19+
20+
it('considers score missing when no score exists', () => {
21+
expect(getScoreLagEpochs({ toEpoch: 100, latestScoreEpoch: null })).toBeNull()
22+
expect(isScoreLagMissing({ toEpoch: 100, latestScoreEpoch: null, allowedLagEpochs: 1 })).toBe(true)
23+
})
24+
})

server/utils/score-freshness.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export interface ScoreFreshnessParams {
2+
toEpoch: number
3+
latestScoreEpoch: number | null
4+
allowedLagEpochs?: number
5+
}
6+
7+
export function getScoreLagEpochs({ toEpoch, latestScoreEpoch }: Pick<ScoreFreshnessParams, 'toEpoch' | 'latestScoreEpoch'>): number | null {
8+
if (latestScoreEpoch === null)
9+
return null
10+
11+
return Math.max(0, toEpoch - latestScoreEpoch)
12+
}
13+
14+
export function isScoreLagMissing({ toEpoch, latestScoreEpoch, allowedLagEpochs = 1 }: ScoreFreshnessParams): boolean {
15+
if (latestScoreEpoch === null)
16+
return true
17+
18+
return (toEpoch - latestScoreEpoch) > allowedLagEpochs
19+
}

server/utils/scores.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { Range, Result, ScoreParams } from 'nimiq-validator-trustscore/types'
22
import type { NewScore } from './drizzle'
3-
import { and, count, desc, eq, gte, lte, or } from 'drizzle-orm'
3+
import { and, desc, eq, gte, lte, or, sql } from 'drizzle-orm'
44
import { getRange } from 'nimiq-validator-trustscore/range'
55
import { computeScore } from 'nimiq-validator-trustscore/score'
66
import { activity } from '../db/schema'
7+
import { isScoreLagMissing } from './score-freshness'
78
import { getStoredValidatorsId } from './validators'
89

910
interface CalculateScoreResult {
@@ -117,12 +118,36 @@ export async function upsertScoresSnapshotEpoch(): Result<CalculateScoreResult>
117118
return [true, undefined, { scores, range }]
118119
}
119120

120-
export async function isMissingScore(range: Range): Promise<boolean> {
121-
const scoreCount = await useDrizzle()
122-
.select({ count: count(tables.scores.epochNumber) })
121+
export async function getLatestScoreEpoch(): Promise<number | null> {
122+
const latestScoreEpoch = await useDrizzle()
123+
.select({
124+
latestScoreEpoch: sql<number>`max(${tables.scores.epochNumber})`,
125+
})
123126
.from(tables.scores)
124-
.where(eq(tables.scores.epochNumber, range.toEpoch))
125127
.get()
126-
.then(res => res?.count || 0)
127-
return scoreCount === 0
128+
.then((res) => {
129+
const value = res?.latestScoreEpoch
130+
if (value === null || value === undefined)
131+
return null
132+
if (typeof value === 'number')
133+
return value
134+
135+
const parsed = Number(value)
136+
return Number.isFinite(parsed) ? parsed : null
137+
})
138+
139+
return latestScoreEpoch
140+
}
141+
142+
export async function isScoreMissingWithLag(
143+
range: Range,
144+
allowedLagEpochs = 1,
145+
latestScoreEpoch?: number | null,
146+
): Promise<boolean> {
147+
const epoch = latestScoreEpoch === undefined ? await getLatestScoreEpoch() : latestScoreEpoch
148+
return isScoreLagMissing({
149+
toEpoch: range.toEpoch,
150+
latestScoreEpoch: epoch,
151+
allowedLagEpochs,
152+
})
128153
}

wrangler.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"main": "dist/server/index.mjs",
55
"assets": { "directory": "dist/public" },
66
"account_id": "cf9baad7d68d7ee717f3339731e81dfb",
7-
"compatibility_date": "2025-01-01",
7+
"compatibility_date": "2026-02-26",
88
"compatibility_flags": ["nodejs_compat"],
99
"observability": {
1010
"enabled": true,

0 commit comments

Comments
 (0)