diff --git a/packages/nimiq-vts/package.json b/packages/nimiq-vts/package.json index 494453e..99fa7ae 100644 --- a/packages/nimiq-vts/package.json +++ b/packages/nimiq-vts/package.json @@ -39,11 +39,13 @@ "prepublishOnly": "nr build", "release": "bumpp && npm publish", "start": "esno src/index.ts", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest" }, "dependencies": { "defu": "^6.1.4", - "nimiq-rpc-client-ts": "^0.3.0" + "nimiq-rpc-client-ts": "^0.3.0", + "vitest": "^1.6.0" }, "devDependencies": { "@antfu/ni": "^0.21.12", diff --git a/packages/nimiq-vts/src/score.test.ts b/packages/nimiq-vts/src/score.test.ts new file mode 100644 index 0000000..478d0fa --- /dev/null +++ b/packages/nimiq-vts/src/score.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it, beforeEach } from 'vitest' +import { getLiveness } from './score' + +describe('should compute livenes correctly', () => { + const fromEpoch = 3075210 + const toEpoch = 5062410 + const blocksPerEpoch = 43200 + const activeEpochBlockNumbers = Array.from({ length: Math.floor((toEpoch - fromEpoch) / blocksPerEpoch) }, (_, i) => fromEpoch + i * blocksPerEpoch) + const weightFactor = 0.5 + + it('exported', () => { + const liveness = getLiveness({fromEpoch, toEpoch, weightFactor, activeEpochBlockNumbers, blocksPerEpoch }) + expect(liveness).toBeGreaterThan(0.5) + }) +}) diff --git a/packages/nimiq-vts/src/score.ts b/packages/nimiq-vts/src/score.ts index 0e2badb..d5d69fd 100644 --- a/packages/nimiq-vts/src/score.ts +++ b/packages/nimiq-vts/src/score.ts @@ -9,28 +9,34 @@ export function getSize({ balance, threshold, steepness, totalBalance }: ScorePa return s } +// TODO: active epoch numbers convert to binary array export function getLiveness({ activeEpochBlockNumbers, blocksPerEpoch, fromEpoch, toEpoch, weightFactor }: ScoreParams['liveness']) { if (!activeEpochBlockNumbers || !fromEpoch || !toEpoch || !weightFactor || !blocksPerEpoch) - throw new Error("Active epoch block numbers, from epoch, to epoch, blocks per epoch, or weight factor is not set") + throw new Error(`Invalid params: ${JSON.stringify({ activeEpochBlockNumbers, blocksPerEpoch, fromEpoch, toEpoch, weightFactor })}`) if (fromEpoch === -1 || toEpoch === -1 || activeEpochBlockNumbers.length === 0) throw new Error(`fromEpoch, toEpoch, or activeEpochBlockNumbers is not set: ${fromEpoch}, ${toEpoch}, ${activeEpochBlockNumbers}`) + if (toEpoch - fromEpoch + 1 <= 0) + throw new Error(`Invalid epoch range. fromEpoch: ${fromEpoch}, toEpoch: ${toEpoch}`) - const n = toEpoch - fromEpoch + 1; // Total number of epochs in the window - if (n <= 0) throw new Error('Invalid epoch range'); + let weightedSum = 0 + let weightTotal = 0 - let weightedSum = 0; - let weightTotal = 0; - - for (let i = toEpoch; i >= fromEpoch; i--) { - const isActive = activeEpochBlockNumbers.includes(i) ? 1 : 0; - const weight = 1 - weightFactor * (i - fromEpoch) / (toEpoch - fromEpoch); - weightedSum += weight * isActive; - weightTotal += weight; + const n = toEpoch - fromEpoch // Total number of epochs in the window + const indexToBlockNumber = (i: number) => fromEpoch + i * blocksPerEpoch + console.log({ fromEpoch, toEpoch, weightFactor, n }) + for (let i = 0; i <= n; i++) { + const index = indexToBlockNumber(i) + const isActive = activeEpochBlockNumbers.indexOf(index) ? 1 : 0 + const weight = 1 - weightFactor * index / n + weightedSum += weight * isActive + weightTotal += weight + if(i < 20) + console.log(JSON.stringify({ i, isActive, weight, weightedSum, weightTotal, x: weightFactor * i / n },null, 2)) } - if (weightTotal === 0) throw new Error('Weight total is zero, cannot divide by zero'); + if (weightTotal === 0) throw new Error('Weight total is zero, cannot divide by zero') - const movingAverage = weightedSum / weightTotal; - const liveness = -Math.pow(movingAverage, 2) + 2 * movingAverage; + const movingAverage = weightedSum / weightTotal + const liveness = -Math.pow(movingAverage, 2) + 2 * movingAverage return liveness } diff --git a/packages/nimiq-vts/src/types.ts b/packages/nimiq-vts/src/types.ts index f34c98e..f6bc50c 100644 --- a/packages/nimiq-vts/src/types.ts +++ b/packages/nimiq-vts/src/types.ts @@ -4,6 +4,7 @@ export type ScoreParams = { reliability?: {} } export type ActivityEpoch = { validator: string, assigned: number, missed: number }[] +// TODO: rename ValidatorActivity to be more descriptive export type ValidatorActivity = Record export type EpochActivity = Record export type ScoreValues = { liveness: number, reliability: number, size: number, total: number } diff --git a/packages/nimiq-vts/src/utils.ts b/packages/nimiq-vts/src/utils.ts index 123711c..a1095a7 100644 --- a/packages/nimiq-vts/src/utils.ts +++ b/packages/nimiq-vts/src/utils.ts @@ -25,6 +25,7 @@ export async function getRange(client: Client, options?: GetRangeOptions): Promi if (errorCurrentEpoch || !currentEpoch) throw new Error(errorCurrentEpoch?.message || 'No current epoch') toEpochIndex = currentEpoch - 1 } + // Don't go back more than the block after the genesis block const fromEpochIndex = Math.max(1, toEpochIndex - epochsCount) // Convert indexes to election blocks diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 743473b..f94f006 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,6 +123,9 @@ importers: nimiq-rpc-client-ts: specifier: ^0.3.0 version: 0.3.0 + vitest: + specifier: ^1.6.0 + version: 1.6.0(@types/node@20.12.12) devDependencies: '@antfu/ni': specifier: ^0.21.12 @@ -1672,6 +1675,13 @@ packages: wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi@7.0.0 + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: false + /@jridgewell/gen-mapping@0.3.5: resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -2021,7 +2031,7 @@ packages: semver: 7.6.2 simple-git: 3.24.0 sirv: 2.0.4 - unimport: 3.7.1 + unimport: 3.7.1(rollup@4.18.0) vite: 5.2.11(@types/node@20.12.12) vite-plugin-inspect: 0.8.4(@nuxt/kit@3.11.2)(vite@5.2.11) vite-plugin-vue-inspector: 5.1.1(vite@5.2.11) @@ -2116,7 +2126,7 @@ packages: get-port-please: 3.1.2 mlly: 1.7.0 pathe: 1.1.2 - unimport: 3.7.1 + unimport: 3.7.1(rollup@4.18.0) transitivePeerDependencies: - bufferutil - nuxt @@ -2183,7 +2193,7 @@ packages: semver: 7.6.2 ufo: 1.5.3 unctx: 2.3.1 - unimport: 3.7.1 + unimport: 3.7.1(rollup@4.18.0) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -2202,7 +2212,7 @@ packages: scule: 1.3.0 std-env: 3.7.0 ufo: 1.5.3 - unimport: 3.7.1 + unimport: 3.7.1(rollup@4.18.0) untyped: 1.4.2 transitivePeerDependencies: - rollup @@ -2243,7 +2253,7 @@ packages: vue: ^3.3.4 dependencies: '@nuxt/kit': 3.11.2 - '@rollup/plugin-replace': 5.0.5 + '@rollup/plugin-replace': 5.0.5(rollup@4.18.0) '@vitejs/plugin-vue': 5.0.4(vite@5.2.11)(vue@3.4.27) '@vitejs/plugin-vue-jsx': 3.1.0(vite@5.2.11)(vue@3.4.27) autoprefixer: 10.4.19(postcss@8.4.38) @@ -2273,7 +2283,7 @@ packages: unenv: 1.9.0 unplugin: 1.10.1 vite: 5.2.11(@types/node@20.12.12) - vite-node: 1.6.0 + vite-node: 1.6.0(@types/node@20.12.12) vite-plugin-checker: 0.6.4(eslint@9.3.0)(typescript@5.4.5)(vite@5.2.11) vue: 3.4.27(typescript@5.4.5) vue-bundle-renderer: 2.1.0 @@ -2797,18 +2807,6 @@ packages: resolve: 1.22.8 rollup: 4.18.0 - /@rollup/plugin-replace@5.0.5: - resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) - magic-string: 0.30.10 - /@rollup/plugin-replace@5.0.5(rollup@3.29.4): resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} engines: {node: '>=14.0.0'} @@ -2880,6 +2878,7 @@ packages: estree-walker: 2.0.2 picomatch: 2.3.1 rollup: 3.29.4 + dev: true /@rollup/pluginutils@5.1.0(rollup@4.18.0): resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} @@ -3068,6 +3067,10 @@ packages: '@sigstore/core': 1.1.0 '@sigstore/protobuf-specs': 0.3.2 + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: false + /@sindresorhus/is@4.6.0: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -3585,7 +3588,7 @@ packages: hasBin: true dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@unocss/config': 0.58.9 '@unocss/core': 0.58.9 '@unocss/preset-uno': 0.58.9 @@ -3606,7 +3609,7 @@ packages: hasBin: true dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@unocss/config': 0.60.3 '@unocss/core': 0.60.3 '@unocss/preset-uno': 0.60.3 @@ -3979,7 +3982,7 @@ packages: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@unocss/config': 0.58.9 '@unocss/core': 0.58.9 '@unocss/inspector': 0.58.9 @@ -3998,7 +4001,7 @@ packages: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@unocss/config': 0.60.3 '@unocss/core': 0.60.3 '@unocss/inspector': 0.60.3 @@ -4018,7 +4021,7 @@ packages: webpack: ^4 || ^5 dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@unocss/config': 0.60.3 '@unocss/core': 0.60.3 chokidar: 3.6.0 @@ -4081,6 +4084,45 @@ packages: vite: 5.2.11(@types/node@20.12.12) vue: 3.4.27(typescript@5.4.5) + /@vitest/expect@1.6.0: + resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + dependencies: + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + chai: 4.4.1 + dev: false + + /@vitest/runner@1.6.0: + resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + dependencies: + '@vitest/utils': 1.6.0 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: false + + /@vitest/snapshot@1.6.0: + resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + dependencies: + magic-string: 0.30.10 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: false + + /@vitest/spy@1.6.0: + resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + dependencies: + tinyspy: 2.2.1 + dev: false + + /@vitest/utils@1.6.0: + resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: false + /@vue-macros/common@1.10.3(vue@3.4.27): resolution: {integrity: sha512-YSgzcbXrRo8a/TF/YIguqEmTld1KA60VETKJG8iFuaAfj7j+Tbdin3cj7/cYbcCHORSq1v9IThgq7r8keH7LXQ==} engines: {node: '>=16.14.0'} @@ -4091,7 +4133,7 @@ packages: optional: true dependencies: '@babel/types': 7.24.5 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@vue/compiler-sfc': 3.4.27 ast-kit: 0.12.1 local-pkg: 0.5.0 @@ -4563,7 +4605,6 @@ packages: /acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} - dev: true /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} @@ -4644,6 +4685,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: false + /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -4707,6 +4753,10 @@ packages: printable-characters: 1.0.42 dev: true + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: false + /ast-kit@0.12.1: resolution: {integrity: sha512-O+33g7x6irsESUcd47KdfWUrS2F6aGp9KeVJFGj0YjIznfXpBxVGjA0w+y/1OKqX4mFOfmZ9Xpf1ixPT4n9xxw==} engines: {node: '>=16.14.0'} @@ -4719,7 +4769,7 @@ packages: engines: {node: '>=16.14.0'} dependencies: '@babel/parser': 7.24.5 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) pathe: 1.1.2 transitivePeerDependencies: - rollup @@ -5008,6 +5058,19 @@ packages: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} dev: true + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: false + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -5060,6 +5123,12 @@ packages: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} dev: true + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: false + /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -5604,6 +5673,13 @@ packages: dev: true optional: true + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: false + /deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -5690,6 +5766,11 @@ packages: dequal: 2.0.3 dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + /diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} @@ -6981,6 +7062,10 @@ packages: engines: {node: '>=18'} dev: true + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: false + /get-port-please@3.1.2: resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} @@ -8049,6 +8134,12 @@ packages: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} dev: true + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: false + /lru-cache@10.2.2: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} @@ -9292,7 +9383,7 @@ packages: uncrypto: 0.1.3 unctx: 2.3.1 unenv: 1.9.0 - unimport: 3.7.1 + unimport: 3.7.1(rollup@4.18.0) unplugin: 1.10.1 unplugin-vue-router: 0.7.0(vue-router@4.3.2)(vue@3.4.27) unstorage: 1.10.2(ioredis@5.4.1) @@ -9462,6 +9553,13 @@ packages: dependencies: yocto-queue: 1.0.0 + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: false + /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -9631,6 +9729,10 @@ packages: /pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: false + /perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -10292,6 +10394,15 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + dev: false + /printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} dev: true @@ -10397,6 +10508,10 @@ packages: dev: true optional: true + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + dev: false + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -10757,6 +10872,7 @@ packages: hasBin: true optionalDependencies: fsevents: 2.3.3 + dev: true /rollup@4.18.0: resolution: {integrity: sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==} @@ -10940,6 +11056,10 @@ packages: '@shikijs/core': 1.6.0 dev: true + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -11174,6 +11294,10 @@ packages: dependencies: minipass: 7.1.1 + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: false + /stacktracey@2.1.8: resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} dependencies: @@ -11494,6 +11618,20 @@ packages: /tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + /tinybench@2.8.0: + resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} + dev: false + + /tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + dev: false + + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + dev: false + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -11600,7 +11738,6 @@ packages: /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - dev: true /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} @@ -11747,25 +11884,6 @@ packages: vfile: 6.0.1 dev: true - /unimport@3.7.1: - resolution: {integrity: sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==} - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) - acorn: 8.11.3 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - fast-glob: 3.3.2 - local-pkg: 0.5.0 - magic-string: 0.30.10 - mlly: 1.7.0 - pathe: 1.1.2 - pkg-types: 1.1.1 - scule: 1.3.0 - strip-literal: 1.3.0 - unplugin: 1.10.1 - transitivePeerDependencies: - - rollup - /unimport@3.7.1(rollup@4.18.0): resolution: {integrity: sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==} dependencies: @@ -11952,7 +12070,7 @@ packages: optional: true dependencies: '@babel/types': 7.24.5 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@vue-macros/common': 1.10.3(vue@3.4.27) ast-walker-scope: 0.5.0 chokidar: 3.6.0 @@ -12151,7 +12269,7 @@ packages: dependencies: vite: 5.2.11(@types/node@20.12.12) - /vite-node@1.6.0: + /vite-node@1.6.0(@types/node@20.12.12): resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -12233,7 +12351,7 @@ packages: dependencies: '@antfu/utils': 0.7.8 '@nuxt/kit': 3.11.2 - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) debug: 4.3.4 error-stack-parser-es: 0.1.4 fs-extra: 11.2.0 @@ -12321,6 +12439,62 @@ packages: optionalDependencies: fsevents: 2.3.3 + /vitest@1.6.0(@types/node@20.12.12): + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.12.12 + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.2.11(@types/node@20.12.12) + vite-node: 1.6.0(@types/node@20.12.12) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: false + /vscode-jsonrpc@6.0.0: resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} engines: {node: '>=8.0.0 || >=10.0.0'} @@ -12580,6 +12754,15 @@ packages: dependencies: isexe: 3.1.1 + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: false + /wicked-good-xpath@1.3.0: resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} dev: true diff --git a/server/database/utils.ts b/server/database/utils.ts index 726b714..88dfdeb 100644 --- a/server/database/utils.ts +++ b/server/database/utils.ts @@ -4,6 +4,11 @@ import type { EpochActivity, Range, ValidatorActivity } from "nimiq-vts" import Identicons from '@nimiq/identicons' import { NewScore, NewValidator } from "../utils/drizzle" +/** + * Given a list of validator addresses, it returns the addresses that are missing in the database. + * This is useful when we are fetching the activity for a range of epochs and we need to check if the validators are already in the database. + * They should be present in the database because the fetch function needs to be run in order to compute the score. + */ export async function getMissingValidators(addresses: string[]) { const existingAddresses = await useDrizzle() .select({ address: tables.validators.address }) @@ -45,10 +50,10 @@ export async function storeValidator(address: string, rest: Omit v.address) const missingValidators = await getMissingValidators(addresses) - if (missingValidators.length > 0) throw new Error(`Missing validators: ${missingValidators.join(', ')}`) + if (missingValidators.length > 0) throw new Error(`Missing validators in database: ${missingValidators.join(', ')}. Run the fetch task first.`) const missingEpochs = await getMissingEpochs(range) - if (missingEpochs.length > 0) throw new Error(`Missing epochs: ${missingEpochs.join(', ')}`) + if (missingEpochs.length > 0) throw new Error(`Missing epochs in database: ${missingEpochs.join(', ')}. Run the fetch task first.`) const activities = await useDrizzle() .select({ @@ -59,7 +64,8 @@ export async function getActivityByValidator(validators: { address: string, bala .from(tables.activity) .innerJoin(tables.validators, eq(tables.activity.validatorId, tables.validators.id)) .where(and( - lte(tables.activity.epochBlockNumber, range.toEpoch), gte(tables.activity.epochBlockNumber, range.fromEpoch), + // Get epochs in the range and for the given validators + gte(tables.activity.epochBlockNumber, range.fromEpoch), lte(tables.activity.epochBlockNumber, range.toEpoch), inArray(tables.validators.address, addresses) )) .execute() diff --git a/server/tasks/score.ts b/server/tasks/score.ts index 46a257c..ad83d08 100644 --- a/server/tasks/score.ts +++ b/server/tasks/score.ts @@ -18,10 +18,10 @@ export default defineTask({ consola.info(`Fetching data for range: ${JSON.stringify(range)}`) // When we compute the score, we only compute the score for the current active validators - const { data: activeValidators, error: errorActiveValdators } = await client.blockchain.getActiveValidators() - if (errorActiveValdators || !activeValidators) throw new Error(errorActiveValdators.message || 'No active validators') + const { data: activeValidators, error: errorActiveValidators } = await client.blockchain.getActiveValidators() + if (errorActiveValidators || !activeValidators) throw new Error(errorActiveValidators.message || 'No active validators') - // Get the activity for the range. If there is missing validators or activity + // Get the activity for the range. If there is missing validators or activity it will throw an error const activity = await getActivityByValidator(activeValidators, range) const totalBalance = activeValidators.reduce((acc, v) => acc + v.balance, 0)