diff --git a/content/validator-trust-score.md b/content/validator-trust-score.md
index ea22346..341bdde 100644
--- a/content/validator-trust-score.md
+++ b/content/validator-trust-score.md
@@ -2,14 +2,66 @@
The Nimiq's Validator Trust Score is a metric that helps users to evaluate the trustworthiness of a validator. It is a value between 0 and 1, where 0 means that the validator is not trustworthy at all and 1 means that the validator behaves well and is trustworthy.
-> The score is just a value used to help users evaluate the trustworthiness of a validator. It is not a guarantee that the validator is trustworthy and the value of the score can change over time. The value of the score won't affect the protocol in any way.
-
The score is reactive and changes over time as the validator behaves well or poorly.
-This score is a combination of several factors that we believe are important for a validator to be trustworthy. These factors include liveness, uptime and age. In this document, we will explain how the score is calculated and what each factor means.
+The factors are, respectively: reliability, liveness and size. All of the factors are in the range $$[0; 1]$$, so the trust score naturally is also in the range (that then we can transform into whichever range we want of course).
-Feel free to share your feedback and suggestions on how to improve the trust score calculation.
+$$
+T = R \times L \times S
+$$
+
+## Reliability
## Liveness
-This factor measures how often the validator is online and producing blocks. A validator that is offline for a long time is not trustworthy. The liveness factor is calculated as follows:
+The Reliability factor penalizes validators that inconsistently produce blocks when expected, assessing their active contribution to the network.
+
+```text
+For a single batch, take the number of slots that the validator owns and received a reward for and divide it by the number of slots that the validator owns.
+```
+
+We calculate each observation (data point) of the reliability like this:
+
+To calculate the factor we do a weighted moving average over the past $n$ batches:
+
+$$
+\bar{R} = \frac{\sum_{i=0}^{n-1} \left( 1-a\frac{i}{n-1} \right) x_i}{\sum_{i=0}^{n-1} \left( 1-a\frac{i}{n-1} \right)}
+$$
+
+Where $x_i$ is the observation at batch number $i$ with $i=0$ representing the most recent batch. And $a$ is a parameter determining how much the observation of the oldest batch is worth relative to the observation of the newest batch.
+
+We decided the parameters to be:
+
+$$
+a=0.5
+batches\_in\_a\_day = 60 * 60 * 24;
+d = 9 * 30; \text{ 9 months in days}
+$$
+
+$$
+
+n = batches_in_a_day \* d
+
+$$
+
+### **Adjusting for High-Reliability Expectations**
+
+The previous formula provides a weighted moving average score for server reliability in block production, where a score of $0.9$ indicates a significant downtime of 10%, highlighting recent performance and the need for improved consistency. (This is not entirely true, but it is a good approximation)
+
+As one block is missing, this is a significant shortfall. To better reflect the high standards required, we will plot the value on a circle, having $c$ as the parameter defining the slope of the arc.
+
+$$
+
+R=-c+1-\sqrt{-\bar{R}^{2}+2c\bar{R}+\left(c-1\right)^{2}}
+\\~\\
+\text{Center of circle at (c,-c+1), where }
+c=-0.16
+
+$$
+
+
+
+Feel free to share your feedback and suggestions on how to improve the trust score calculation.
+
+$$
+$$
diff --git a/nuxt.config.ts b/nuxt.config.ts
index e63052e..7b14894 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -104,5 +104,12 @@ export default defineNuxtConfig({
type: true,
},
]
+ },
+
+ content: {
+ markdown: {
+ remarkPlugins: ['remark-math'],
+ rehypePlugins: ['rehype-mathjax'],
+ }
}
})
diff --git a/package.json b/package.json
index 2fdb7aa..05a3710 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,8 @@
"eslint": "^9.2.0",
"eslint-plugin-format": "^0.1.1",
"nimiq-css": "^0.0.96",
+ "rehype-mathjax": "^6.0.0",
+ "remark-math": "^6.0.0",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0",
"wrangler": "^3.56.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e33d503..4ec8905 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -90,6 +90,12 @@ importers:
nimiq-css:
specifier: ^0.0.96
version: 0.0.96(typescript@5.4.5)(unocss@0.60.2(@unocss/webpack@0.60.2(rollup@4.17.2)(webpack@5.91.0(esbuild@0.17.19)))(postcss@8.4.38)(rollup@4.17.2)(vite@5.2.11(@types/node@20.12.12)(terser@5.31.0)))(vite@5.2.11(@types/node@20.12.12)(terser@5.31.0))
+ rehype-mathjax:
+ specifier: ^6.0.0
+ version: 6.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ remark-math:
+ specifier: ^6.0.0
+ version: 6.0.0
vite-plugin-top-level-await:
specifier: ^1.4.1
version: 1.4.1(rollup@4.17.2)(vite@5.2.11(@types/node@20.12.12)(terser@5.31.0))
@@ -165,6 +171,9 @@ packages:
resolution: {integrity: sha512-DxjgKBCoyReu4p5HMvpmgSOfRhhBcuf5V5soDDRgOTZMwsA4KSFzol1abFZgiCTE11L2kKGca5Md9GwDdXVBwQ==}
engines: {node: '>= 16'}
+ '@asamuzakjp/dom-selector@2.0.2':
+ resolution: {integrity: sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==}
+
'@babel/code-frame@7.24.2':
resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
engines: {node: '>=6.9.0'}
@@ -1751,6 +1760,12 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+ '@types/katex@0.16.7':
+ resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
+
+ '@types/mathjax@0.0.40':
+ resolution: {integrity: sha512-rHusx08LCg92WJxrsM3SPjvLTSvK5C+gealtSuhKbEOcUZfWlwigaFoPLf6Dfxhg4oryN5qP9Sj7zOQ4HYXINw==}
+
'@types/mdast@3.0.15':
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
@@ -2340,6 +2355,9 @@ packages:
async@3.2.5:
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
autoprefixer@10.4.19:
resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
engines: {node: ^10 || ^12 || >=14}
@@ -2374,6 +2392,9 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -2596,6 +2617,10 @@ packages:
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
comlink@4.4.1:
resolution: {integrity: sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==}
@@ -2613,6 +2638,10 @@ packages:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
+ commander@9.2.0:
+ resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==}
+ engines: {node: ^12.20.0 || >=14}
+
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
@@ -2740,6 +2769,10 @@ packages:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+ cssstyle@4.0.1:
+ resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==}
+ engines: {node: '>=18'}
+
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -2754,6 +2787,10 @@ packages:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
+ data-urls@5.0.0:
+ resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
+ engines: {node: '>=18'}
+
db0@0.1.4:
resolution: {integrity: sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==}
peerDependencies:
@@ -2793,6 +2830,9 @@ packages:
supports-color:
optional: true
+ decimal.js@10.4.3:
+ resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
+
decode-named-character-reference@1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
@@ -2834,6 +2874,10 @@ packages:
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
@@ -3344,6 +3388,10 @@ packages:
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
+ esm@3.2.25:
+ resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
+ engines: {node: '>=6'}
+
esniff@2.0.1:
resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
engines: {node: '>=0.10'}
@@ -3509,6 +3557,10 @@ packages:
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
engines: {node: '>=14'}
+ form-data@4.0.0:
+ resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
+ engines: {node: '>= 6'}
+
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
@@ -3687,6 +3739,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
+ hast-util-from-dom@5.0.0:
+ resolution: {integrity: sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==}
+
hast-util-from-parse5@8.0.1:
resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==}
@@ -3708,6 +3763,9 @@ packages:
hast-util-to-string@3.0.0:
resolution: {integrity: sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==}
+ hast-util-to-text@4.0.2:
+ resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
+
hastscript@8.0.0:
resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==}
@@ -3724,6 +3782,10 @@ packages:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
+ html-encoding-sniffer@4.0.0:
+ resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+ engines: {node: '>=18'}
+
html-tags@3.3.1:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'}
@@ -3925,6 +3987,9 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-primitive@3.0.1:
resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==}
engines: {node: '>=0.10.0'}
@@ -4000,6 +4065,15 @@ packages:
resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==}
engines: {node: '>=12.0.0'}
+ jsdom@23.2.0:
+ resolution: {integrity: sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ canvas: ^2.11.2
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jsesc@0.5.0:
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
hasBin: true
@@ -4053,6 +4127,10 @@ packages:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0}
+ katex@0.16.10:
+ resolution: {integrity: sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==}
+ hasBin: true
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -4184,6 +4262,9 @@ packages:
markdown-table@3.0.3:
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
+ mathjax-full@3.2.2:
+ resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==}
+
mdast-util-find-and-replace@3.0.1:
resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==}
@@ -4211,6 +4292,9 @@ packages:
mdast-util-gfm@3.0.0:
resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==}
+ mdast-util-math@3.0.0:
+ resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==}
+
mdast-util-phrasing@4.1.0:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
@@ -4245,6 +4329,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
+ mhchemparser@4.2.1:
+ resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==}
+
micromark-core-commonmark@2.0.1:
resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==}
@@ -4269,6 +4356,9 @@ packages:
micromark-extension-gfm@3.0.0:
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
+ micromark-extension-math@3.0.0:
+ resolution: {integrity: sha512-iJ2Q28vBoEovLN5o3GO12CpqorQRYDPT+p4zW50tGwTfJB+iv/VnB6Ini+gqa24K97DwptMBBIvVX6Bjk49oyQ==}
+
micromark-factory-destination@2.0.0:
resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==}
@@ -4442,6 +4532,9 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+ mj-context-menu@0.6.1:
+ resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==}
+
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
@@ -5101,6 +5194,9 @@ packages:
protocols@2.0.1:
resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==}
+ psl@1.9.0:
+ resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
+
pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
@@ -5108,6 +5204,9 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ querystringify@2.2.0:
+ resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
+
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -5187,6 +5286,9 @@ packages:
rehype-external-links@3.0.0:
resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
+ rehype-mathjax@6.0.0:
+ resolution: {integrity: sha512-SioRmn+0mRWtDc4QVKG9JG88bXhPazfhc11GQoQ68mwot2WWyfabyZ7tuJu3Z4LCf893wXkQTVTF8PUlntoDwA==}
+
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
@@ -5206,6 +5308,9 @@ packages:
remark-gfm@4.0.0:
resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==}
+ remark-math@6.0.0:
+ resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==}
+
remark-mdc@3.2.1:
resolution: {integrity: sha512-MLNqQE7ryygOA3TtH4hKmIvmjFAqTMzCs2zrMzXs4MWJXYM2vbtdwR2NfgcN3vxIp5Pllgq3oLGuKgQSs8J19w==}
@@ -5222,6 +5327,13 @@ packages:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ requires-port@1.0.0:
+ resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -5281,6 +5393,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
+ rrweb-cssom@0.6.0:
+ resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
+
run-applescript@7.0.0:
resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
engines: {node: '>=18'}
@@ -5297,6 +5412,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
schema-utils@3.3.0:
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
engines: {node: '>= 10.13.0'}
@@ -5480,6 +5599,10 @@ packages:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
+ speech-rule-engine@4.0.7:
+ resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==}
+ hasBin: true
+
splitpanes@3.1.5:
resolution: {integrity: sha512-r3Mq2ITFQ5a2VXLOy4/Sb2Ptp7OfEO8YIbhVJqJXoFc9hc5nTXXkCvtVDjIGbvC0vdE7tse+xTM9BMjsszP6bw==}
@@ -5602,6 +5725,9 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
synckit@0.6.2:
resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==}
engines: {node: '>=12.20'}
@@ -5699,9 +5825,17 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
+ tough-cookie@4.1.4:
+ resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
+ engines: {node: '>=6'}
+
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ tr46@5.0.0:
+ resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
+ engines: {node: '>=18'}
+
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@@ -5819,12 +5953,18 @@ packages:
unist-builder@4.0.0:
resolution: {integrity: sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==}
+ unist-util-find-after@5.0.0:
+ resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
+
unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
unist-util-position@5.0.0:
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+ unist-util-remove-position@5.0.0:
+ resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==}
+
unist-util-stringify-position@2.0.3:
resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==}
@@ -5837,6 +5977,10 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
+ universalify@0.2.0:
+ resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
+ engines: {node: '>= 4.0.0'}
+
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -5932,6 +6076,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ url-parse@1.5.10:
+ resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+
urlpattern-polyfill@8.0.2:
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
@@ -6156,6 +6303,10 @@ packages:
typescript:
optional: true
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
watchpack@2.4.1:
resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==}
engines: {node: '>=10.13.0'}
@@ -6170,6 +6321,10 @@ packages:
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
+
webpack-sources@3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
engines: {node: '>=10.13.0'}
@@ -6191,6 +6346,18 @@ packages:
resolution: {integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==}
engines: {node: '>=4.0.0'}
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+
+ whatwg-mimetype@4.0.0:
+ resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+ engines: {node: '>=18'}
+
+ whatwg-url@14.0.0:
+ resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==}
+ engines: {node: '>=18'}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -6214,6 +6381,9 @@ packages:
engines: {node: '>=8'}
hasBin: true
+ wicked-good-xpath@1.3.0:
+ resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==}
+
wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
@@ -6278,6 +6448,17 @@ packages:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
+ xmldom-sre@0.1.31:
+ resolution: {integrity: sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==}
+ engines: {node: '>=0.1'}
+
xmlhttprequest-ssl@2.0.0:
resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==}
engines: {node: '>=0.4.0'}
@@ -6417,6 +6598,12 @@ snapshots:
'@types/json-schema': 7.0.15
js-yaml: 4.1.0
+ '@asamuzakjp/dom-selector@2.0.2':
+ dependencies:
+ bidi-js: 1.0.3
+ css-tree: 2.3.1
+ is-potential-custom-element-name: 1.0.1
+
'@babel/code-frame@7.24.2':
dependencies:
'@babel/highlight': 7.24.5
@@ -8170,7 +8357,7 @@ snapshots:
'@types/hast@3.0.4':
dependencies:
- '@types/unist': 2.0.10
+ '@types/unist': 3.0.2
'@types/http-proxy@1.17.14':
dependencies:
@@ -8178,13 +8365,17 @@ snapshots:
'@types/json-schema@7.0.15': {}
+ '@types/katex@0.16.7': {}
+
+ '@types/mathjax@0.0.40': {}
+
'@types/mdast@3.0.15':
dependencies:
'@types/unist': 2.0.10
'@types/mdast@4.0.4':
dependencies:
- '@types/unist': 2.0.10
+ '@types/unist': 3.0.2
'@types/ms@0.7.34': {}
@@ -9065,6 +9256,8 @@ snapshots:
async@3.2.5: {}
+ asynckit@0.4.0: {}
+
autoprefixer@10.4.19(postcss@8.4.38):
dependencies:
browserslist: 4.23.0
@@ -9106,6 +9299,10 @@ snapshots:
base64-js@1.5.1: {}
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
+
binary-extensions@2.3.0: {}
bindings@1.5.0:
@@ -9361,6 +9558,10 @@ snapshots:
colorette@2.0.20: {}
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
comlink@4.4.1: {}
comma-separated-tokens@2.0.3: {}
@@ -9371,6 +9572,8 @@ snapshots:
commander@8.3.0: {}
+ commander@9.2.0: {}
+
commander@9.5.0: {}
comment-parser@1.4.1: {}
@@ -9503,6 +9706,10 @@ snapshots:
dependencies:
css-tree: 2.2.1
+ cssstyle@4.0.1:
+ dependencies:
+ rrweb-cssom: 0.6.0
+
csstype@3.1.3: {}
d@1.0.2:
@@ -9514,6 +9721,11 @@ snapshots:
data-uri-to-buffer@4.0.1: {}
+ data-urls@5.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.0.0
+
db0@0.1.4(drizzle-orm@0.30.10(@cloudflare/workers-types@4.20240512.0)(@opentelemetry/api@1.8.0)):
optionalDependencies:
drizzle-orm: 0.30.10(@cloudflare/workers-types@4.20240512.0)(@opentelemetry/api@1.8.0)
@@ -9530,6 +9742,8 @@ snapshots:
dependencies:
ms: 2.1.2
+ decimal.js@10.4.3: {}
+
decode-named-character-reference@1.0.2:
dependencies:
character-entities: 2.0.2
@@ -9564,6 +9778,8 @@ snapshots:
defu@6.1.4: {}
+ delayed-stream@1.0.0: {}
+
delegates@1.0.0: {}
denque@2.1.0: {}
@@ -10188,6 +10404,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ esm@3.2.25: {}
+
esniff@2.0.1:
dependencies:
d: 1.0.2
@@ -10375,6 +10593,12 @@ snapshots:
cross-spawn: 7.0.3
signal-exit: 4.1.0
+ form-data@4.0.0:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ mime-types: 2.1.35
+
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
@@ -10579,6 +10803,12 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ hast-util-from-dom@5.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ hastscript: 8.0.0
+ web-namespaces: 2.0.1
+
hast-util-from-parse5@8.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -10632,6 +10862,13 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
+ hast-util-to-text@4.0.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.2
+ hast-util-is-element: 3.0.0
+ unist-util-find-after: 5.0.0
+
hastscript@8.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -10650,6 +10887,10 @@ snapshots:
dependencies:
lru-cache: 10.2.2
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
html-tags@3.3.1: {}
html-void-elements@3.0.0: {}
@@ -10698,7 +10939,6 @@ snapshots:
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
- optional: true
ieee754@1.2.1: {}
@@ -10860,6 +11100,8 @@ snapshots:
is-plain-obj@4.1.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-primitive@3.0.1: {}
is-promise@2.2.2: {}
@@ -10922,6 +11164,34 @@ snapshots:
jsdoc-type-pratt-parser@4.0.0: {}
+ jsdom@23.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+ dependencies:
+ '@asamuzakjp/dom-selector': 2.0.2
+ cssstyle: 4.0.1
+ data-urls: 5.0.0
+ decimal.js: 10.4.3
+ form-data: 4.0.0
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.4
+ is-potential-custom-element-name: 1.0.1
+ parse5: 7.1.2
+ rrweb-cssom: 0.6.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 4.1.4
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.0.0
+ ws: 8.17.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
jsesc@0.5.0: {}
jsesc@2.5.2: {}
@@ -10966,6 +11236,10 @@ snapshots:
jsonparse@1.3.1: {}
+ katex@0.16.10:
+ dependencies:
+ commander: 8.3.0
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -11116,6 +11390,13 @@ snapshots:
markdown-table@3.0.3: {}
+ mathjax-full@3.2.2:
+ dependencies:
+ esm: 3.2.25
+ mhchemparser: 4.2.1
+ mj-context-menu: 0.6.1
+ speech-rule-engine: 4.0.7
+
mdast-util-find-and-replace@3.0.1:
dependencies:
'@types/mdast': 4.0.4
@@ -11207,6 +11488,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ mdast-util-math@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ longest-streak: 3.1.0
+ mdast-util-from-markdown: 2.0.0
+ mdast-util-to-markdown: 2.1.0
+ unist-util-remove-position: 5.0.0
+ transitivePeerDependencies:
+ - supports-color
+
mdast-util-phrasing@4.1.0:
dependencies:
'@types/mdast': 4.0.4
@@ -11262,6 +11555,8 @@ snapshots:
merge2@1.4.1: {}
+ mhchemparser@4.2.1: {}
+
micromark-core-commonmark@2.0.1:
dependencies:
decode-named-character-reference: 1.0.2
@@ -11339,6 +11634,16 @@ snapshots:
micromark-util-combine-extensions: 2.0.0
micromark-util-types: 2.0.0
+ micromark-extension-math@3.0.0:
+ dependencies:
+ '@types/katex': 0.16.7
+ devlop: 1.1.0
+ katex: 0.16.10
+ micromark-factory-space: 2.0.0
+ micromark-util-character: 2.1.0
+ micromark-util-symbol: 2.0.0
+ micromark-util-types: 2.0.0
+
micromark-factory-destination@2.0.0:
dependencies:
micromark-util-character: 2.1.0
@@ -11568,6 +11873,8 @@ snapshots:
mitt@3.0.1: {}
+ mj-context-menu@0.6.1: {}
+
mkdirp-classic@0.5.3:
optional: true
@@ -12429,6 +12736,8 @@ snapshots:
protocols@2.0.1: {}
+ psl@1.9.0: {}
+
pump@3.0.0:
dependencies:
end-of-stream: 1.4.4
@@ -12437,6 +12746,8 @@ snapshots:
punycode@2.3.1: {}
+ querystringify@2.2.0: {}
+
queue-microtask@1.2.3: {}
queue-tick@1.0.1: {}
@@ -12540,6 +12851,22 @@ snapshots:
space-separated-tokens: 2.0.2
unist-util-visit: 5.0.0
+ rehype-mathjax@6.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mathjax': 0.0.40
+ hast-util-from-dom: 5.0.0
+ hast-util-to-text: 4.0.2
+ jsdom: 23.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ mathjax-full: 3.2.2
+ unified: 11.0.4
+ unist-util-visit-parents: 6.0.1
+ transitivePeerDependencies:
+ - bufferutil
+ - canvas
+ - supports-color
+ - utf-8-validate
+
rehype-raw@7.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -12584,6 +12911,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ remark-math@6.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-math: 3.0.0
+ micromark-extension-math: 3.0.0
+ unified: 11.0.4
+ transitivePeerDependencies:
+ - supports-color
+
remark-mdc@3.2.1:
dependencies:
'@types/mdast': 4.0.4
@@ -12632,6 +12968,10 @@ snapshots:
require-directory@2.1.1: {}
+ require-from-string@2.0.2: {}
+
+ requires-port@1.0.0: {}
+
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
@@ -12701,6 +13041,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.17.2
fsevents: 2.3.3
+ rrweb-cssom@0.6.0: {}
+
run-applescript@7.0.0: {}
run-parallel@1.2.0:
@@ -12711,8 +13053,11 @@ snapshots:
safe-buffer@5.2.1: {}
- safer-buffer@2.1.2:
- optional: true
+ safer-buffer@2.1.2: {}
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
schema-utils@3.3.0:
dependencies:
@@ -12934,6 +13279,12 @@ snapshots:
speakingurl@14.0.1: {}
+ speech-rule-engine@4.0.7:
+ dependencies:
+ commander: 9.2.0
+ wicked-good-xpath: 1.3.0
+ xmldom-sre: 0.1.31
+
splitpanes@3.1.5: {}
sprintf-js@1.1.3: {}
@@ -13055,6 +13406,8 @@ snapshots:
csso: 5.0.5
picocolors: 1.0.1
+ symbol-tree@3.2.4: {}
+
synckit@0.6.2:
dependencies:
tslib: 2.6.2
@@ -13161,8 +13514,19 @@ snapshots:
totalist@3.0.1: {}
+ tough-cookie@4.1.4:
+ dependencies:
+ psl: 1.9.0
+ punycode: 2.3.1
+ universalify: 0.2.0
+ url-parse: 1.5.10
+
tr46@0.0.3: {}
+ tr46@5.0.0:
+ dependencies:
+ punycode: 2.3.1
+
trim-lines@3.0.1: {}
trough@2.2.0: {}
@@ -13296,6 +13660,11 @@ snapshots:
dependencies:
'@types/unist': 3.0.2
+ unist-util-find-after@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.2
+ unist-util-is: 6.0.0
+
unist-util-is@6.0.0:
dependencies:
'@types/unist': 3.0.2
@@ -13304,6 +13673,11 @@ snapshots:
dependencies:
'@types/unist': 3.0.2
+ unist-util-remove-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.2
+ unist-util-visit: 5.0.0
+
unist-util-stringify-position@2.0.3:
dependencies:
'@types/unist': 2.0.10
@@ -13323,6 +13697,8 @@ snapshots:
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
+ universalify@0.2.0: {}
+
universalify@2.0.1: {}
unocss@0.60.2(@unocss/webpack@0.60.2(rollup@4.17.2)(webpack@5.91.0(esbuild@0.17.19)))(postcss@8.4.38)(rollup@4.17.2)(vite@5.2.11(@types/node@20.12.12)(terser@5.31.0)):
@@ -13439,6 +13815,11 @@ snapshots:
dependencies:
punycode: 2.3.1
+ url-parse@1.5.10:
+ dependencies:
+ querystringify: 2.2.0
+ requires-port: 1.0.0
+
urlpattern-polyfill@8.0.2: {}
utf-8-validate@5.0.10:
@@ -13683,6 +14064,10 @@ snapshots:
optionalDependencies:
typescript: 5.4.5
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
watchpack@2.4.1:
dependencies:
glob-to-regexp: 0.4.1
@@ -13694,6 +14079,8 @@ snapshots:
webidl-conversions@3.0.1: {}
+ webidl-conversions@7.0.0: {}
+
webpack-sources@3.2.3: {}
webpack-virtual-modules@0.6.1: {}
@@ -13740,6 +14127,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ whatwg-encoding@3.1.1:
+ dependencies:
+ iconv-lite: 0.6.3
+
+ whatwg-mimetype@4.0.0: {}
+
+ whatwg-url@14.0.0:
+ dependencies:
+ tr46: 5.0.0
+ webidl-conversions: 7.0.0
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
@@ -13763,6 +14161,8 @@ snapshots:
stackback: 0.0.2
optional: true
+ wicked-good-xpath@1.3.0: {}
+
wide-align@1.1.5:
dependencies:
string-width: 4.2.3
@@ -13829,6 +14229,12 @@ snapshots:
xml-name-validator@4.0.0: {}
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
+ xmldom-sre@0.1.31: {}
+
xmlhttprequest-ssl@2.0.0: {}
xss@1.0.15:
diff --git a/server/database/migrations/0000_awesome_warbound.sql b/server/database/migrations/0000_rare_the_enforcers.sql
similarity index 89%
rename from server/database/migrations/0000_awesome_warbound.sql
rename to server/database/migrations/0000_rare_the_enforcers.sql
index d0a812e..235694e 100644
--- a/server/database/migrations/0000_awesome_warbound.sql
+++ b/server/database/migrations/0000_rare_the_enforcers.sql
@@ -1,9 +1,9 @@
CREATE TABLE `activity` (
`validator_id` integer NOT NULL,
- `epoch_index` integer NOT NULL,
+ `epoch_block_number` integer NOT NULL,
`assigned` integer NOT NULL,
`missed` integer NOT NULL,
- PRIMARY KEY(`epoch_index`, `validator_id`),
+ PRIMARY KEY(`epoch_block_number`, `validator_id`),
FOREIGN KEY (`validator_id`) REFERENCES `validators`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
@@ -14,7 +14,6 @@ CREATE TABLE `scores` (
`liveness` real NOT NULL,
`size` real NOT NULL,
`reliability` real NOT NULL,
- `updated_at` text DEFAULT (CURRENT_TIMESTAMP),
FOREIGN KEY (`validator_id`) REFERENCES `validators`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
diff --git a/server/database/migrations/meta/0000_snapshot.json b/server/database/migrations/meta/0000_snapshot.json
index 5cfd5a3..855953c 100644
--- a/server/database/migrations/meta/0000_snapshot.json
+++ b/server/database/migrations/meta/0000_snapshot.json
@@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
- "id": "a933a4be-af11-4e85-af36-1103ab9c4196",
+ "id": "e0c818d9-d1cc-4bab-8acb-3da68fdbdb69",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"activity": {
@@ -14,8 +14,8 @@
"notNull": true,
"autoincrement": false
},
- "epoch_index": {
- "name": "epoch_index",
+ "epoch_block_number": {
+ "name": "epoch_block_number",
"type": "integer",
"primaryKey": false,
"notNull": true,
@@ -53,12 +53,12 @@
}
},
"compositePrimaryKeys": {
- "activity_validator_id_epoch_index_pk": {
+ "activity_validator_id_epoch_block_number_pk": {
"columns": [
- "epoch_index",
+ "epoch_block_number",
"validator_id"
],
- "name": "activity_validator_id_epoch_index_pk"
+ "name": "activity_validator_id_epoch_block_number_pk"
}
},
"uniqueConstraints": {}
@@ -107,14 +107,6 @@
"primaryKey": false,
"notNull": true,
"autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false,
- "default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
diff --git a/server/database/migrations/meta/_journal.json b/server/database/migrations/meta/_journal.json
index 7ddd3c0..407c499 100644
--- a/server/database/migrations/meta/_journal.json
+++ b/server/database/migrations/meta/_journal.json
@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
- "when": 1716374392871,
- "tag": "0000_awesome_warbound",
+ "when": 1716452825470,
+ "tag": "0000_rare_the_enforcers",
"breakpoints": true
}
]
diff --git a/server/database/schema.ts b/server/database/schema.ts
index a2fbc37..b1bb3a5 100644
--- a/server/database/schema.ts
+++ b/server/database/schema.ts
@@ -20,12 +20,11 @@ export const scores = sqliteTable('scores', {
liveness: real('liveness').notNull(),
size: real('size').notNull(),
reliability: real('reliability').notNull(),
- updatedAt: text("updated_at").default(sql`(CURRENT_TIMESTAMP)`),
})
export const activity = sqliteTable('activity', {
validatorId: integer('validator_id').notNull().references(() => validators.id),
- epochIndex: integer('epoch_index').notNull(),
+ epochBlockNumber: integer('epoch_block_number').notNull(),
assigned: integer('assigned').notNull(),
missed: integer('missed').notNull(),
-}, ({ epochIndex, validatorId }) => ({ pk: primaryKey({ columns: [validatorId, epochIndex] }) }))
+}, ({ epochBlockNumber, validatorId }) => ({ pk: primaryKey({ columns: [validatorId, epochBlockNumber] }) }))
diff --git a/server/database/seed.ts b/server/database/seed.ts
index acb2d30..8614ec8 100644
--- a/server/database/seed.ts
+++ b/server/database/seed.ts
@@ -1,12 +1,15 @@
import { NewValidator } from "../utils/drizzle";
import { getMissingValidators, storeValidator } from "./utils";
+import { consola } from 'consola'
export async function seedDatabase() {
+ consola.info('Seeding database with validators...')
for (const validator of validators) {
- if ((await getMissingValidators([validator.address])).length > 0)
+ const alreadyInDatabase = await getMissingValidators([validator.address]).then(r => r.length === 0)
+ if (alreadyInDatabase)
await useDrizzle().update(tables.validators).set({ ...validator }).where(eq(tables.validators.address, validator.address)).execute()
else
- await storeValidator(validator.address)
+ await storeValidator(validator.address, validator)
}
}
diff --git a/server/database/utils.ts b/server/database/utils.ts
index d143165..d39b135 100644
--- a/server/database/utils.ts
+++ b/server/database/utils.ts
@@ -1,8 +1,8 @@
import { gte, inArray, lte } from "drizzle-orm"
-import { ActivityEpoch, EpochRange, ScoreValues, ValidatorEpochs } from "../vts/types"
+import { EpochActivity, Range, ValidatorActivity } from "../vts/types"
// @ts-expect-error no types
import Identicons from '@nimiq/identicons'
-import { NewScore } from "../utils/drizzle"
+import { NewScore, NewValidator } from "../utils/drizzle"
export async function getMissingValidators(addresses: string[]) {
const existingAddresses = await useDrizzle()
@@ -15,9 +15,11 @@ export async function getMissingValidators(addresses: string[]) {
return missingAddresses
}
+// A simple cache to avoid querying the database multiple times for the same validator
+// Useful when we are fetching batches of activities for the same validator across multiple epochs
const validators = new Map()
-export async function storeValidator(address: string) {
+export async function storeValidator(address: string, rest: Omit = {}) {
if (validators.has(address)) return validators.get(address) as number
const validatorId = await useDrizzle()
.select({ id: tables.validators.id })
@@ -31,79 +33,93 @@ export async function storeValidator(address: string) {
}
const icon = await Identicons.default.toDataUrl(address) as string
- const newValidator = await useDrizzle().insert(tables.validators).values({ address, icon }).returning().get()
+ const newValidator = await useDrizzle().insert(tables.validators).values({ address, icon, ...rest }).returning().get()
validators.set(address, newValidator.id)
return newValidator.id
}
/**
- * It gets the list of active validators and all its required data in order to be able to compute the score.
- * If there is a validator that is not in the database, it throws an error.
+ * Give a list of validator addresses and a range of epochs, it returns the activity for the given validators and epochs.
+ * If there are missing validators or epochs, it will throw an error.
*/
-export async function getEpochIndexes(range: EpochRange): Promise {
+export async function getActivityByValidator(validators: { address: string, balance: number }[], range: Range) {
+ const addresses = validators.map(v => v.address)
+ const missingValidators = await getMissingValidators(addresses)
+ if (missingValidators.length > 0) throw new Error(`Missing validators: ${missingValidators.join(', ')}`)
+
const missingEpochs = await getMissingEpochs(range)
if (missingEpochs.length > 0) throw new Error(`Missing epochs: ${missingEpochs.join(', ')}`)
const activities = await useDrizzle()
.select({
- epochIndex: tables.activity.epochIndex,
+ blockNumber: tables.activity.epochBlockNumber,
address: tables.validators.address,
validatorId: tables.validators.id
})
.from(tables.activity)
.innerJoin(tables.validators, eq(tables.activity.validatorId, tables.validators.id))
- .where(and(lte(tables.activity.epochIndex, range.toEpoch), gte(tables.activity.epochIndex, range.fromEpoch)))
+ .where(and(
+ lte(tables.activity.epochBlockNumber, range.toEpoch), gte(tables.activity.epochBlockNumber, range.fromEpoch),
+ inArray(tables.validators.address, addresses)
+ ))
.execute()
const activitiesByValidator = activities.reduce((acc, activity) => {
- if (!acc[activity.address]) acc[activity.address] = { validatorId: activity.validatorId, activeEpochIndexes: [] }
- acc[activity.address].activeEpochIndexes.push(activity.epochIndex)
+ const balance = validators.find(v => v.address === activity.address)?.balance
+ if (!balance) throw new Error(`No balance for validator ${activity.address}`)
+ if (!acc[activity.address]) acc[activity.address] = { validatorId: activity.validatorId, balance, activeEpochBlockNumbers: [] }
+ acc[activity.address].activeEpochBlockNumbers.push(activity.blockNumber)
return acc
- }, {} as Record)
+ }, {} as ValidatorActivity)
return activitiesByValidator
}
/**
* Given a range of epochs, it returns the epochs that are missing in the database.
*/
-export async function getMissingEpochs(range: EpochRange) {
+export async function getMissingEpochs(range: Range) {
const existingEpochs = await useDrizzle()
- .select({ epochIndex: tables.activity.epochIndex })
+ .select({ epochBlockNumber: tables.activity.epochBlockNumber })
.from(tables.activity)
- .where(and(gte(tables.activity.epochIndex, range.fromEpoch), lte(tables.activity.epochIndex, range.toEpoch)))
- .execute().then(r => r.map(r => r.epochIndex))
- return Array.from({ length: range.totalEpochs }, (_, i) => range.fromEpoch + i).filter(epoch => existingEpochs.indexOf(epoch) === -1)
+ .where(and(gte(tables.activity.epochBlockNumber, range.fromEpoch), lte(tables.activity.epochBlockNumber, range.toEpoch)))
+ .execute().then(r => r.map(r => r.epochBlockNumber))
+
+ const missingEpochs = []
+ for (let i = range.fromEpoch; i <= range.toEpoch; i += range.blocksPerEpoch) {
+ if (existingEpochs.indexOf(i) === -1) missingEpochs.push(i)
+ }
+ return missingEpochs
}
/**
* It computes the score for a given range of epochs. It will fetch the activity for the given epochs and then compute the score for each validator.
* It will delete the activities for the given epochs and then insert the new activities.
*/
-export async function storeActivities(activities: Record) {
+export async function storeActivities(activities: EpochActivity) {
+
const values: Newactivity[] = []
- const epochIndexes = Object.keys(activities).map(Number)
- for (const _epochIndex of epochIndexes) {
- const epochIndex = Number(_epochIndex)
- for (const { assigned, missed, validator } of activities[epochIndex]) {
+ const blockNumbers = Object.keys(activities).map(Number)
+ for (const _epochBlockNumber of blockNumbers) {
+ const epochBlockNumber = Number(_epochBlockNumber)
+ for (const { assigned, missed, validator } of activities[epochBlockNumber]) {
const validatorId = await storeValidator(validator)
- values.push({ assigned, missed, epochIndex, validatorId })
+ values.push({ assigned, missed, epochBlockNumber, validatorId })
}
}
- console.log('Deleting activities for epochs:', epochIndexes)
- await useDrizzle().delete(tables.activity).where(inArray(tables.activity.epochIndex, epochIndexes))
+ await useDrizzle().delete(tables.activity).where(inArray(tables.activity.epochBlockNumber, blockNumbers))
// For some reason, D1 is hanging when inserting all the values at once. So dividing the values in chunks of 32
- // seems to work
+ // seems to work: https://github.com/prisma/prisma/discussions/23646#discussioncomment-9083299
const chunkArray = (arr: any[], chunkSize: number) => Array.from({ length: Math.ceil(arr.length / chunkSize) }, (_, i) => arr.slice(i * chunkSize, i * chunkSize + chunkSize))
- for (const chunk of chunkArray(values, 32))
- await Promise.all(values.map(v => useDrizzle().insert(tables.activity).values(chunk)))
+ for (const chunk of chunkArray(values, 16))
+ await useDrizzle().insert(tables.activity).values(chunk)
- // If we ever move out of cloudfare we could use transactions to avoid inconsistencies
+ // If we ever move out of cloudfare we could use transactions to avoid inconsistencies and improve performance
// Cloudfare D1 does not support transactions: https://github.com/cloudflare/workerd/blob/e78561270004797ff008f17790dae7cfe4a39629/src/workerd/api/sql-test.js#L252-L253
// await useDrizzle().transaction(async (tx) => {
- // await tx.delete(tables.activity).where(inArray(tables.activity.epochIndex, epochIndexes))
+ // await tx.delete(tables.activity).where(inArray(tables.activity.epochBlockNumbers, blockNumbers))
// await Promise.all(values.map(v => tx.insert(tables.activity).values(v)))
// })
}
@@ -112,10 +128,8 @@ export async function storeActivities(activities: Record)
* Insert the scores into the database. To avoid inconsistencies, it deletes all the scores for the given validators and then inserts the new scores.
*/
export async function storeScores(scores: NewScore[]) {
- const updatedAt = new Date().toISOString()
-
await useDrizzle().delete(tables.scores).where(or(...scores.map(({ validatorId }) => eq(tables.scores.validatorId, validatorId))))
- await useDrizzle().insert(tables.scores).values(scores.map(s => ({ ...s, updatedAt })))
+ await useDrizzle().insert(tables.scores).values(scores)
// If we ever move out of cloudfare we could use transactions to avoid inconsistencies
// Cloudfare D1 does not support transactions: https://github.com/cloudflare/workerd/blob/e78561270004797ff008f17790dae7cfe4a39629/src/workerd/api/sql-test.js#L252-L253
diff --git a/server/plugins/seed.ts b/server/plugins/seed.ts
deleted file mode 100644
index f92c1db..0000000
--- a/server/plugins/seed.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { seedDatabase } from '../database/seed'
-
-export default defineNitroPlugin(async () => {
- if (!import.meta.dev) return
- onHubReady(seedDatabase)
-})
diff --git a/server/tasks/fetch.ts b/server/tasks/fetch.ts
index 856e376..e0870dc 100644
--- a/server/tasks/fetch.ts
+++ b/server/tasks/fetch.ts
@@ -1,6 +1,6 @@
import { Client } from 'nimiq-rpc-client-ts'
import { fetchEpochsActivity } from '../vts/fetcher'
-import { getEpochRange } from '../vts/utils'
+import { getRange } from '../vts/utils'
import { getMissingEpochs, storeActivities } from '../database/utils'
import { consola } from 'consola'
@@ -15,20 +15,20 @@ export default defineTask({
const client = new Client(new URL(useRuntimeConfig().rpcUrl))
// The range that we will consider
- const range = await getEpochRange(client)
+ const range = await getRange(client)
consola.info(`Fetching data for range: ${JSON.stringify(range)}`)
// Only fetch the missing epochs that are not in the database
- const epochsIndexes = await getMissingEpochs(range)
- consola.info(`Fetching data for epochs: ${JSON.stringify(epochsIndexes)}`)
- if (epochsIndexes.length === 0) return { success: "No epochs to fetch. Database is up to date" }
+ const epochBlockNumbers = await getMissingEpochs(range)
+ consola.info(`Fetching data for epochs: ${JSON.stringify(epochBlockNumbers)}`)
+ if (epochBlockNumbers.length === 0) return { success: "No epochs to fetch. Database is up to date" }
// Fetch the activity for the given epochs
- const activities = await fetchEpochsActivity(client, epochsIndexes)
- consola.info(`Fetched data for ${epochsIndexes.length} epochs`)
+ const activities = await fetchEpochsActivity(client, epochBlockNumbers)
+ consola.info(`Fetched data for ${epochBlockNumbers.length} epochs`)
await storeActivities(activities)
- consola.success(`Stored data for ${epochsIndexes.length} epochs. (${range.totalEpochs - epochsIndexes.length} epochs were already in the database)`)
+ consola.success(`Stored data for ${epochBlockNumbers.length} epochs.`)
return { result: "success" }
},
diff --git a/server/tasks/reset.ts b/server/tasks/reset.ts
deleted file mode 100644
index a2c679e..0000000
--- a/server/tasks/reset.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export default defineTask({
- meta: {
- name: "db:reset",
- description: "Deletes all data from the database",
- },
- async run() {
- console.log("deleting DB...")
- const events = await useDrizzle().delete(tables.activity).get()
- const scores = await useDrizzle().delete(tables.scores).get()
- const validators = await useDrizzle().delete(tables.validators).get()
- return { result: { events, scores, validators } }
- },
-})
diff --git a/server/tasks/score.ts b/server/tasks/score.ts
index 86d995d..1743edf 100644
--- a/server/tasks/score.ts
+++ b/server/tasks/score.ts
@@ -1,9 +1,9 @@
import { consola } from 'consola'
import { Client } from 'nimiq-rpc-client-ts'
import { computeScore } from '../vts/score'
-import { getEpochRange, getValidatorsParams } from '../vts/utils'
+import { getRange } from '../vts/utils'
import { NewScore } from '../utils/drizzle'
-import { storeScores } from '../database/utils'
+import { getActivityByValidator, storeScores } from '../database/utils'
export default defineTask({
meta: {
@@ -15,18 +15,25 @@ export default defineTask({
const client = new Client(new URL(useRuntimeConfig().rpcUrl))
// The range that we will consider
- const range = await getEpochRange(client)
+ const range = await getRange(client)
consola.info(`Fetching data for range: ${JSON.stringify(range)}`)
- // Get the parameters for the validators
- const params = await getValidatorsParams(client, range)
- consola.info(`Prepared params for score computation: ${JSON.stringify(params)}`)
+ // 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')
- // Compute the scores
- const scoresValues = params.map(p => ({ validatorId: p.validatorId, ...computeScore(p.params) } satisfies NewScore))
- await storeScores(scoresValues)
- consola.success(`Stored scores for ${scoresValues.length} validators`)
+ // Get the activity for the range. If there is missing validators or activity
+ const activity = await getActivityByValidator(activeValidators, range)
+ const totalBalance = activeValidators.reduce((acc, v) => acc + v.balance, 0)
- return { result: scoresValues }
+ consola.info(`Computing score for: ${Object.keys(activity).join(', ')}`)
+ const scores = Object.values(activity).map(({ activeEpochBlockNumbers, validatorId, balance }) => {
+ const score = computeScore({ liveness: { activeEpochBlockNumbers, ...range }, size: { balance, totalBalance } })
+ return { validatorId, ...score } satisfies NewScore
+ })
+ await storeScores(scores)
+ consola.success(`Stored scores for ${scores.length} validators`)
+
+ return { result: scores }
},
})
diff --git a/server/tasks/seed.ts b/server/tasks/seed.ts
new file mode 100644
index 0000000..46925b2
--- /dev/null
+++ b/server/tasks/seed.ts
@@ -0,0 +1,14 @@
+import { seedDatabase } from "../database/seed"
+import { consola } from 'consola'
+
+export default defineTask({
+ meta: {
+ name: "db:seed",
+ description: "Deletes all data from the database",
+ },
+ async run() {
+ consola.info("Seeding validators table...")
+ await seedDatabase()
+ return { result: "Database seeded" }
+ },
+})
diff --git a/server/vts/fetcher.ts b/server/vts/fetcher.ts
index bb64f8d..5e59263 100644
--- a/server/vts/fetcher.ts
+++ b/server/vts/fetcher.ts
@@ -1,4 +1,4 @@
-import { Client, ElectionMacroBlock, PolicyConstants } from "nimiq-rpc-client-ts";
+import { Client, ElectionMacroBlock } from "nimiq-rpc-client-ts";
import { ActivityEpoch } from "./types";
/**
@@ -14,24 +14,9 @@ export async function fetchValidatorSlotsAssignation(client: Client, blockNumber
}
/**
- * Fetches the activity for a range of epochs and stores it in the database.
- *
- * It will fetch the activity epoch by epoch and store it in the database. It will go from the last epoch to the first epoch.
- *
- * @param client The RPC client
- * @param customRange The range to fetch the activity. Optional.
+ * Fetches the activity for the given block numbers.
*/
-export async function fetchEpochsActivity(client: Client, epochsIndexes: number[]) {
- const { data: policy, error: errorPolicy } = await client.policy.getPolicyConstants()
- if (errorPolicy) throw new Error(JSON.stringify({ errorPolicy, policy }))
- const { blocksPerEpoch, genesisBlockNumber } = policy as PolicyConstants & { genesisBlockNumber: number }
-
- const activities: Record = {}
-
- const toBlockNumber = (epochIndex: number) => genesisBlockNumber + epochIndex * blocksPerEpoch
-
- for (const epochIndex of epochsIndexes)
- activities[epochIndex] = await fetchValidatorSlotsAssignation(client, toBlockNumber(epochIndex), 0)
-
- return activities
+export async function fetchEpochsActivity(client: Client, epochBlockNumbers: number[]): Promise> {
+ const promises = epochBlockNumbers.map(async blockNumber => [blockNumber, await fetchValidatorSlotsAssignation(client, blockNumber, 0)])
+ return Object.fromEntries(await Promise.all(promises))
}
diff --git a/server/vts/score.ts b/server/vts/score.ts
index 7d05cd3..0e2badb 100644
--- a/server/vts/score.ts
+++ b/server/vts/score.ts
@@ -1,16 +1,19 @@
-import { ComputeScoreConst, ScoreValues } from './types'
+import { ScoreParams, ScoreValues } from './types'
import { defu } from "defu"
-export function getSize({ balance, threshold, steepness, totalBalance }: Required) {
+export function getSize({ balance, threshold, steepness, totalBalance }: ScoreParams['size']) {
+ if (!balance || !threshold || !steepness || !totalBalance) throw new Error("Balance, threshold, steepness, or total balance is not set")
if (balance < 0 || totalBalance < 0) throw new Error("Balance or total balance is negative")
const size = balance / totalBalance
const s = Math.max(0, 1 - Math.pow(size / threshold, steepness))
return s
}
-export function getLiveness({ activeEpochIndexes, fromEpoch, toEpoch, weightFactor }: Required) {
- if (fromEpoch === -1 || toEpoch === -1 || activeEpochIndexes.length === 0)
- throw new Error(`fromEpoch, toEpoch, or activeEpochIndexes is not set: ${fromEpoch}, ${toEpoch}, ${activeEpochIndexes}`)
+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")
+ if (fromEpoch === -1 || toEpoch === -1 || activeEpochBlockNumbers.length === 0)
+ throw new Error(`fromEpoch, toEpoch, or activeEpochBlockNumbers is not set: ${fromEpoch}, ${toEpoch}, ${activeEpochBlockNumbers}`)
const n = toEpoch - fromEpoch + 1; // Total number of epochs in the window
if (n <= 0) throw new Error('Invalid epoch range');
@@ -18,24 +21,21 @@ export function getLiveness({ activeEpochIndexes, fromEpoch, toEpoch, weightFact
let weightedSum = 0;
let weightTotal = 0;
- for (let i = 0; i < n; i++) {
- const epochIndex = fromEpoch + i;
- const isActive = activeEpochIndexes.includes(epochIndex) ? 1 : 0;
- const weight = 1 - weightFactor * (i / (n - 1));
+ 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;
}
-
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;
return liveness
}
-// export async function getReliability({ }: Required) {
+// export async function getReliability({ }: Required) {
// TODO
// }
export async function getReliability({ }: any) {
@@ -45,14 +45,14 @@ export async function getReliability({ }: any) {
// The default values for the computeScore function
// Negative values and empty arrays are used to indicate that the user must provide the values or an error will be thrown
-const defaultComputeScoreConst: ComputeScoreConst = {
+const defaultScoreParams: ScoreParams = {
size: { threshold: 0.25, steepness: 4, balance: -1, totalBalance: -1 },
- liveness: { weightFactor: 0.5, fromEpoch: -1, toEpoch: -1, activeEpochIndexes: [] as number[] },
+ liveness: { weightFactor: 0.5, fromEpoch: -1, toEpoch: -1, activeEpochBlockNumbers: [] as number[] },
reliability: {}
}
-export function computeScore(params: ComputeScoreConst) {
- const computeScoreParams = defu(params, defaultComputeScoreConst)
+export function computeScore(params: ScoreParams) {
+ const computeScoreParams = defu(params, defaultScoreParams)
const size = getSize(computeScoreParams.size)
const liveness = getLiveness(computeScoreParams.liveness)
diff --git a/server/vts/types.ts b/server/vts/types.ts
index c929b77..ba99880 100644
--- a/server/vts/types.ts
+++ b/server/vts/types.ts
@@ -1,10 +1,18 @@
-export type ComputeScoreConst = {
- liveness: Partial<{ weightFactor: number, fromEpoch: number, toEpoch: number, activeEpochIndexes: number[] }>,
+export type ScoreParams = {
+ liveness: Partial<({ weightFactor: number, activeEpochBlockNumbers: number[] }) & Range>,
size: Partial<{ threshold: number, steepness: number, balance: number, totalBalance: number }>,
reliability?: {}
}
-export type ScoreParams = { validatorId: number, params: ComputeScoreConst }
export type ActivityEpoch = { validator: string, assigned: number, missed: number }[]
-export type ValidatorEpochs = Record
+export type ValidatorActivity = Record
+export type EpochActivity = Record
export type ScoreValues = Pick
-export type EpochRange = { fromEpoch: number, toEpoch: number, totalEpochs: number }
+export interface Range {
+ // The first block number that we will consider
+ fromEpoch: number,
+ // The last block number that we will consider
+ toEpoch: number,
+
+ blocksPerEpoch: number,
+}
+
diff --git a/server/vts/utils.ts b/server/vts/utils.ts
index 83f6e92..123711c 100644
--- a/server/vts/utils.ts
+++ b/server/vts/utils.ts
@@ -1,75 +1,44 @@
import { Client, PolicyConstants } from 'nimiq-rpc-client-ts'
-import { EpochRange, ScoreParams } from './types'
-// TODO REmove this
-import { getMissingValidators, getEpochIndexes } from '../database/utils'
+import { Range } from './types'
-/**
- * It gets the list of active validators and all its required data in order to be able to compute the score.
- *
- * Note: For now, we only support the computation of the current epoch, although we can modify the code to
- * support the ability to compute the score for a given epoch. The main problem is that we need to fetch the
- * balances for each validators for the given epoch and currently there is no an easy way to do that.
- */
-export async function getValidatorsParams(client: Client, range: EpochRange) {
- const { data: activeValidators, error: errorActiveValdators } = await client.blockchain.getActiveValidators()
- if (errorActiveValdators || !activeValidators) throw new Error(errorActiveValdators.message || 'No active validators')
-
- const missingValidators = await getMissingValidators(activeValidators.map(v => v.address))
- if (missingValidators.length > 0) throw new Error(`There are missing validators: ${missingValidators.join(', ')}`)
+interface GetRangeOptions {
+ // The last epoch number that we will consider. Default to the last finished epoch.
+ toEpochIndex?: number
+ // The amount of milliseconds we want to consider. Default to 9 months.
+ durationMs?: number
+}
- const totalBalance = activeValidators.reduce((acc, v) => acc + v.balance, 0)
- const validatorBalances = Object.fromEntries(activeValidators.map(v => [v.address, v.balance]))
+/**
+ * Given the amount of milliseconds we want to consider, it returns an object with the epoch range we will consider.
+*/
+export async function getRange(client: Client, options?: GetRangeOptions): Promise {
+ const { data: policy, error: errorPolicy } = await client.policy.getPolicyConstants()
+ if (errorPolicy || !policy) throw new Error(errorPolicy.message || 'No policy constants')
- const validatorEpochs = await getEpochIndexes(range)
+ const { blockSeparationTime, blocksPerEpoch, genesisBlockNumber } = policy as PolicyConstants & { blockSeparationTime: number, genesisBlockNumber: number }
+ const durationMs = options?.durationMs || 1000 * 60 * 60 * 24 * 30 * 9
+ const epochsCount = Math.ceil(durationMs / (blockSeparationTime * blocksPerEpoch))
- const params: ScoreParams[] = []
- for (const { address } of activeValidators) {
- const validatorId = validatorEpochs[address].validatorId
- const activeEpochIndexes = validatorEpochs[address].activeEpochIndexes
- const balance = validatorBalances[address]
- params.push({ validatorId, params: { liveness: { activeEpochIndexes, ...range }, size: { balance, totalBalance } } })
+ let toEpochIndex = options?.toEpochIndex
+ if (!toEpochIndex) {
+ const { data: currentEpoch, error: errorCurrentEpoch } = await client.blockchain.getEpochNumber()
+ if (errorCurrentEpoch || !currentEpoch) throw new Error(errorCurrentEpoch?.message || 'No current epoch')
+ toEpochIndex = currentEpoch - 1
}
+ const fromEpochIndex = Math.max(1, toEpochIndex - epochsCount)
- return params
-}
+ // Convert indexes to election blocks
+ const fromEpoch = genesisBlockNumber + blocksPerEpoch * fromEpochIndex
+ const toEpoch = genesisBlockNumber + blocksPerEpoch * toEpochIndex
-/**
- * Validates the epoch range. The range should start from epoch 1 because the epoch 0 is a special epoch where we store the
- * old blockchain data. The range should finish in the current epoch - 1 as we can only compute the score for finished epochs.
- */
-export async function valdidateEpochRange(client: Client, { fromEpoch, toEpoch }: EpochRange) {
+ // Validate the epoch range
if (fromEpoch < 0 || toEpoch < 0 || fromEpoch > toEpoch) throw new Error(`Invalid epoch range: [${fromEpoch}, ${toEpoch}]`)
if (fromEpoch === 0) throw new Error(`Invalid epoch range: [${fromEpoch}, ${toEpoch}]. The range should start from epoch 1`)
- const { data: epochNumber, error } = await client.blockchain.getEpochNumber()
- if (error || !epochNumber) throw new Error(JSON.stringify({ epochNumber, error }))
- if (toEpoch >= epochNumber) throw new Error(`Invalid epoch range: [${fromEpoch}, ${toEpoch}]. The last valid epoch is ${epochNumber - 1}`)
-}
-
-/**
- * Given the amount of milliseconds we want to consider, it returns the range that we will use to compute the score. By default we will use
- * the last 9 months.
- *
- * First it gets which epoch range we are computing. We convert the window size to a range of [fromEpoch, toEpoch].
- * Then, it checks if we have already fetched the data for all the epochs in the range. If there is a missing epoch, it throws an error.
- *
- * The range will be validated
- */
-export async function getEpochRange(client: Client, ms: number = 9 * 30 * 24 * 60 * 60 * 1000): Promise {
- const { data: policy, error: errorPolicy } = await client.policy.getPolicyConstants()
- if (errorPolicy || !policy) throw new Error(errorPolicy.message || 'No policy constants')
-
- const { blockSeparationTime, blocksPerEpoch } = policy as PolicyConstants & { blockSeparationTime: number }
-
- const { data: currentEpoch, error: errorCurrentEpoch } = await client.blockchain.getEpochNumber()
- if (errorCurrentEpoch || !currentEpoch) throw new Error(errorCurrentEpoch?.message || 'No current epoch')
+ const { data: blockNumber, error: errorBlockNumber } = await client.blockchain.getBlockNumber()
+ if (errorBlockNumber || !blockNumber) throw new Error(errorBlockNumber?.message || 'No block number')
+ if (toEpoch >= blockNumber) throw new Error(`Invalid epoch range: [${fromEpoch}, ${toEpoch}]. The current head is ${blockNumber}`)
- const epochDurationMs = blockSeparationTime * blocksPerEpoch
- const expectedTotalEpochs = Math.ceil(ms / epochDurationMs) // The number of epochs to consider
- const fromEpoch = Math.max(1, currentEpoch - expectedTotalEpochs)
- const toEpoch = currentEpoch - 1
- const totalEpochs = toEpoch - fromEpoch + 1 // The actual number of epochs we will consider
- const range: EpochRange = { fromEpoch, toEpoch, totalEpochs }
- await valdidateEpochRange(client, range)
+ const range: Range = { fromEpoch, toEpoch, blocksPerEpoch }
return range
}
diff --git a/unocss.config.ts b/unocss.config.ts
index e0e81f4..0f75e13 100644
--- a/unocss.config.ts
+++ b/unocss.config.ts
@@ -3,6 +3,16 @@ import { defineConfig, presetAttributify, presetUno } from 'unocss'
import { presetRemToPx } from '@unocss/preset-rem-to-px'
export default defineConfig({
+ content: {
+ pipeline: {
+ include: [
+ // the default
+ /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/, 'content/*.md'
+ ],
+ // exclude files
+ // exclude: []
+ },
+ },
presets: [
presetUno({ attributifyPseudo: true }),
presetNimiq({