pow-equix-wasm 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -65
- package/dist/empreinte.json +7 -2
- package/dist/equix-js.d.ts +3 -0
- package/dist/equix-js.js +12075 -0
- package/dist/index.d.ts +92 -16
- package/dist/index.js +105 -32
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -1,25 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PoW Equi-X en WebAssembly
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://github.com/ContribuLibre/pow-equix-wasm/actions/workflows/verification.yml)
|
|
5
|
+
[](https://github.com/ContribuLibre/pow-equix-wasm/releases)
|
|
6
|
+
[](https://www.npmjs.com/package/pow-equix-wasm)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Preuve de travail **Equi-X**, celle que Tor utilise contre les dénis de service :
|
|
9
|
+
un seul module de 45 Kio résout la preuve dans le navigateur, sur plusieurs
|
|
10
|
+
cœurs, et la vérifie côté serveur en quelques centaines de microsecondes. Là où
|
|
11
|
+
WebAssembly est désactivé, le même module traduit en JavaScript pur prend le
|
|
12
|
+
relais, et le mode dégradé est signalé.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
> reproducible byte for byte. LGPL-3.0.*
|
|
15
|
-
|
|
16
|
-
## Pourquoi Equi-X plutôt que SHA-256
|
|
14
|
+
- **Démo en ligne et calibrage :** <https://contribulibre.github.io/pow-equix-wasm/>
|
|
15
|
+
- **Code source :** <https://github.com/ContribuLibre/pow-equix-wasm>
|
|
16
|
+
- **Paquet npm :** [`pow-equix-wasm`](https://www.npmjs.com/package/pow-equix-wasm)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
> *English summary: Equi-X proof of work (from Tor's Arti project) as a single
|
|
19
|
+
> WebAssembly module, with a pure-JavaScript fallback (wasm2js) where
|
|
20
|
+
> WebAssembly is disabled. It solves in the browser across Web Workers
|
|
21
|
+
> (cancellable, with progress and remaining-time estimate) and verifies
|
|
22
|
+
> server-side in ~0.25 ms per part. Reproducible build. LGPL-3.0.*
|
|
23
|
+
|
|
24
|
+
## Pourquoi Equi-X
|
|
25
|
+
|
|
26
|
+
Une preuve de travail n’a de sens que si l’attaquant ne peut pas la calculer
|
|
27
|
+
beaucoup plus vite que la personne honnête, et si le serveur la vérifie pour
|
|
28
|
+
presque rien. Ordres de grandeur pour une preuve réglée sur **une minute dans un
|
|
29
|
+
navigateur, sur un cœur** :
|
|
30
|
+
|
|
31
|
+
| | SHA-256 (hashcash) | Argon2id (64 Mio par essai) | **Equi-X** |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| Un essai, navigateur | ≈ 0,1 à 10 µs | ≈ 0,3 à 1 s | **≈ 0,4 s** (mesuré) |
|
|
34
|
+
| Mémoire pour résoudre | < 1 Kio | 64 Mio par essai en cours | **≈ 2 Mio** par essai en cours (mesuré) |
|
|
35
|
+
| La même preuve sur matériel optimisé | carte graphique : ≈ 10 ms ; puce de minage : ≈ 1 µs | ≈ 10 à 30 s : la mémoire freine cartes graphiques et puces | **≈ 3 s** en code natif compilé (mesuré) ; programmes aléatoires conçus pour ne pas avantager les cartes graphiques |
|
|
36
|
+
| Avantage du matériel optimisé | × 10³ à × 10⁷ | × 2 à × 5 | **≈ × 20** |
|
|
37
|
+
| Vérifier une preuve | 1 empreinte : ≈ 1 µs, négligeable | 1 essai complet : ≈ 0,1 à 1 s **et** 64 Mio par vérification | **≈ 0,25 ms par part** en WebAssembly, ≈ 0,2 ms en natif (mesuré) ; pas de mémoire à réserver |
|
|
38
|
+
|
|
39
|
+
- **SHA-256** : les cartes graphiques et, pire, les puces de minage Bitcoin la
|
|
40
|
+
calculent des milliers à des millions de fois plus vite qu’un navigateur. Des
|
|
41
|
+
outils existent pour résoudre ces défis en masse.
|
|
42
|
+
- **Argon2id** : la mémoire exigée égalise bien le matériel, mais vérifier coûte
|
|
43
|
+
autant qu’un essai : chaque fausse preuve envoyée au serveur lui coûte des
|
|
44
|
+
dizaines de mégaoctets et des centaines de millisecondes.
|
|
45
|
+
- **Equi-X** combine les deux qualités : un nouveau programme HashX tiré au sort
|
|
46
|
+
à chaque essai et environ 2 Mio de mémoire, mais une vérification qui ne
|
|
47
|
+
refait qu’une poignée d’évaluations.
|
|
48
|
+
|
|
49
|
+
Les chiffres « mesurés » viennent de ce dépôt (portable x86-64 récent) ; les
|
|
50
|
+
autres sont des ordres de grandeur publics, à affiner avec la page de démo.
|
|
23
51
|
|
|
24
52
|
## Protocole
|
|
25
53
|
|
|
@@ -40,24 +68,67 @@ une autre.
|
|
|
40
68
|
|
|
41
69
|
## Utilisation
|
|
42
70
|
|
|
43
|
-
|
|
44
|
-
import { ModuleEquix, construireGraine, hexadecimal, resoudre } from 'pow-equix-wasm'
|
|
71
|
+
### Dans le navigateur
|
|
45
72
|
|
|
46
|
-
|
|
73
|
+
```ts
|
|
74
|
+
import { construireGraine, hexadecimal, resoudre } from 'pow-equix-wasm'
|
|
75
|
+
// Le module est un fichier du paquet, que le bundler publie (ici Vite).
|
|
47
76
|
import urlEquix from 'pow-equix-wasm/equix.wasm?url'
|
|
48
|
-
const octets = new Uint8Array(await (await fetch(urlEquix)).arrayBuffer())
|
|
49
77
|
|
|
78
|
+
const octets = new Uint8Array(await (await fetch(urlEquix)).arrayBuffer())
|
|
50
79
|
const graine = await construireGraine('mon-service/1', JSON.stringify(requete))
|
|
51
80
|
const annulation = new AbortController()
|
|
52
|
-
const { parts
|
|
81
|
+
const { parts } = await resoudre({
|
|
53
82
|
octets, graine, effort: 1, nombre: 4,
|
|
54
83
|
signal: annulation.signal,
|
|
55
|
-
onProgression: ({ essais, parts, memoireOctets }) => { /* … */ },
|
|
84
|
+
onProgression: ({ moteur, essais, parts, dureeMs, restantEstimeMs, memoireOctets }) => { /* … */ },
|
|
56
85
|
})
|
|
57
86
|
envoyer({ ...requete, preuve: hexadecimal(parts) })
|
|
58
87
|
```
|
|
59
88
|
|
|
60
|
-
|
|
89
|
+
`resoudre` répartit les compteurs entre des Web Workers créés depuis un Blob
|
|
90
|
+
(aucun fichier supplémentaire à publier) : par défaut un par cœur annoncé, huit
|
|
91
|
+
au plus, et jamais plus que d’essais attendus (`filsConseilles`). Si les Web
|
|
92
|
+
Workers sont refusés, le calcul continue sur le fil courant. `fils: 0` force ce
|
|
93
|
+
mode. La progression donne le moteur, les essais, les parts trouvées, le temps
|
|
94
|
+
écoulé, une estimation du temps restant mesurée sur l’appareil et la mémoire.
|
|
95
|
+
|
|
96
|
+
### Sans WebAssembly : repli en JavaScript et mode dégradé
|
|
97
|
+
|
|
98
|
+
`pow-equix-wasm/js` fournit le même module traduit en JavaScript pur par wasm2js
|
|
99
|
+
(binaryen) : mêmes preuves, octet pour octet, mais bien plus lent. Il ne pèse
|
|
100
|
+
qu’environ 120 Ko une fois minifié, et n’a à être chargé que s’il sert :
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { estimerDuree, ralentissement, resoudre, webAssemblyDisponible } from 'pow-equix-wasm'
|
|
104
|
+
|
|
105
|
+
const wasm = webAssemblyDisponible()
|
|
106
|
+
const js = wasm ? undefined : (await import('pow-equix-wasm/js')).creerExportsEquixJs
|
|
107
|
+
if (!wasm) {
|
|
108
|
+
// Sans WebAssembly, le JIT est en général coupé aussi : hypothèse prudente.
|
|
109
|
+
const attente = estimerDuree({ effort: 1, nombre: 4, execution: 'jsSansJit' })
|
|
110
|
+
if (attente > 5_000) avertir(`Active WebAssembly pour valider environ ${Math.round(ralentissement('jsSansJit'))} fois plus vite.`)
|
|
111
|
+
}
|
|
112
|
+
const resultat = await resoudre({ octets, js, graine, effort: 1, nombre: 4, onProgression })
|
|
113
|
+
// resultat.moteur vaut 'js' en mode dégradé.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`moteur: 'wasm' | 'js' | 'auto'` impose ou laisse choisir le moteur ;
|
|
117
|
+
`moteurRetenu` dit à l’avance lequel servira. `ModuleEquix.instancier(octets)`
|
|
118
|
+
et `ModuleEquix.depuisJs(creerExportsEquixJs)` donnent chacun un module qui
|
|
119
|
+
résout et vérifie.
|
|
120
|
+
|
|
121
|
+
| Un cœur, portable x86-64 | Un essai | Ralentissement |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| WebAssembly (Chromium, Bun, Node) | ≈ 410 ms | × 1 |
|
|
124
|
+
| JavaScript avec JIT | ≈ 3,8 s (Chromium), 1,5 s (Node) | × 4 à × 9 |
|
|
125
|
+
| JavaScript sans JIT (Node `--jitless`) | ≈ 79 s | ≈ × 190 |
|
|
126
|
+
|
|
127
|
+
Désactiver WebAssembly s’accompagne presque toujours d’un JavaScript sans JIT
|
|
128
|
+
(Tor Browser en mode renforcé, mode Isolement d’iOS) : prévoir un avertissement
|
|
129
|
+
et une difficulté réaliste pour ces personnes.
|
|
130
|
+
|
|
131
|
+
### Côté serveur
|
|
61
132
|
|
|
62
133
|
```ts
|
|
63
134
|
import { readFile } from 'node:fs/promises'
|
|
@@ -68,51 +139,24 @@ const graine = await construireGraine('mon-service/1', JSON.stringify(requeteSan
|
|
|
68
139
|
const valide = module.verifier(graine, depuisHexadecimal(preuve) ?? new Uint8Array(), 1, 4)
|
|
69
140
|
```
|
|
70
141
|
|
|
71
|
-
Une page qui ne peut rien télécharger (ouverte en `file://`, script unique)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
import { octetsEquix } from 'pow-equix-wasm/octets'
|
|
76
|
-
const octets = octetsEquix()
|
|
77
|
-
```
|
|
142
|
+
Une page qui ne peut rien télécharger (ouverte en `file://`, script unique)
|
|
143
|
+
intègre le module en base64 avec `octetsEquix()` de `pow-equix-wasm/octets`.
|
|
78
144
|
|
|
79
145
|
Ce qui reste à la charge de l’appelant, et que le paquet ne fait pas :
|
|
80
146
|
refuser les preuves rejouées, vérifier la fraîcheur de la requête (horodatage
|
|
81
147
|
dans le contenu haché) et limiter le débit des vérifications.
|
|
82
148
|
|
|
83
|
-
### Web Workers
|
|
84
|
-
|
|
85
|
-
`resoudre` répartit les compteurs entre des Web Workers créés depuis un Blob
|
|
86
|
-
(aucun fichier supplémentaire à publier) : par défaut un par cœur annoncé, huit
|
|
87
|
-
au plus, et jamais plus que d’essais attendus. Si les Web Workers sont refusés
|
|
88
|
-
(politique de sécurité, `file://` dans certains navigateurs), le calcul continue
|
|
89
|
-
sur le fil courant, essai par essai. `fils: 0` force ce mode.
|
|
90
|
-
|
|
91
|
-
## Mesures
|
|
92
|
-
|
|
93
|
-
Linux x86_64, un fil, module WebAssembly :
|
|
94
|
-
|
|
95
|
-
| | Chromium | Bun 1.4 |
|
|
96
|
-
|---|---|---|
|
|
97
|
-
| un essai | ≈ 410 ms | ≈ 400 ms |
|
|
98
|
-
| vérification d’une part | ≈ 230 µs | ≈ 250 µs |
|
|
99
|
-
| mémoire par fil | ≈ 1,8 Mio de zone de travail (2,9 Mio pour l’instance) | |
|
|
100
|
-
|
|
101
|
-
Les temps varient fortement d’un appareil à l’autre : c’est tout l’objet de la
|
|
102
|
-
page de démo, qui mesure sur l’appareil utilisé le temps d’une preuve (médiane,
|
|
103
|
-
90ᵉ centile), la mémoire par fil et le coût de la vérification, et exporte le
|
|
104
|
-
tout en JSON pour comparer plusieurs appareils.
|
|
105
|
-
|
|
106
149
|
## Construction reproductible
|
|
107
150
|
|
|
108
|
-
|
|
109
|
-
par la CI.
|
|
151
|
+
Rien de construit n’est versionné : `dist/` et `site/` sont produits par la CI.
|
|
110
152
|
|
|
111
153
|
- Rust figé par `rust-toolchain.toml`, dépendances verrouillées par `Cargo.lock`
|
|
112
154
|
(`cargo build --locked`), chemins locaux effacés du binaire
|
|
113
|
-
(`--remap-path-prefix`), pas de compilation incrémentale
|
|
114
|
-
|
|
115
|
-
|
|
155
|
+
(`--remap-path-prefix`), pas de compilation incrémentale ; binaryen, donc
|
|
156
|
+
wasm2js, figé dans `package.json`.
|
|
157
|
+
- `bun run verify:reproductible` reconstruit le module et sa traduction
|
|
158
|
+
JavaScript depuis une copie des sources placée ailleurs, et exige des
|
|
159
|
+
fichiers identiques à l’octet près.
|
|
116
160
|
- Chaque release GitHub donne le SHA-256 du module publié ; pour le vérifier :
|
|
117
161
|
`bun run build && bun run verify:reproductible --attendu <sha256>`.
|
|
118
162
|
|
|
@@ -127,11 +171,10 @@ Il faut Rust (via rustup, qui installe seul la version figée et la cible
|
|
|
127
171
|
|
|
128
172
|
## Publication
|
|
129
173
|
|
|
130
|
-
- **Démo** : chaque push sur `main` la publie sur GitHub Pages
|
|
131
|
-
*Settings → Pages → Source : GitHub Actions*).
|
|
174
|
+
- **Démo** : chaque push sur `main` la publie sur GitHub Pages.
|
|
132
175
|
- **npm** : pousser un tag `vX.Y.Z` égal à la version de `package.json`. Le
|
|
133
|
-
workflow vérifie tout, publie sans aucun secret (publication de confiance
|
|
134
|
-
|
|
176
|
+
workflow vérifie tout, publie sans aucun secret (publication de confiance npm
|
|
177
|
+
par OIDC, avec provenance signée), puis crée la release GitHub avec
|
|
135
178
|
`equix.wasm` et son empreinte.
|
|
136
179
|
|
|
137
180
|
## Organisation
|
|
@@ -141,7 +184,8 @@ Il faut Rust (via rustup, qui installe seul la version figée et la cible
|
|
|
141
184
|
dans une application Tauri).
|
|
142
185
|
- `crates/pow-equix-wasm` : interface C minimale vers WebAssembly, sans
|
|
143
186
|
wasm-bindgen.
|
|
144
|
-
- `src/index.ts` : chargeur, Web Workers, annulation,
|
|
187
|
+
- `src/index.ts` : chargeur, deux moteurs, Web Workers, annulation,
|
|
188
|
+
progression, estimations, vérification.
|
|
145
189
|
- `demo/` : page de démo et de calibrage.
|
|
146
190
|
|
|
147
191
|
## Licence
|
package/dist/empreinte.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.2.0",
|
|
3
3
|
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)",
|
|
4
4
|
"octets": 44968,
|
|
5
|
-
"sha256": "f6c72e8e6b48386c23b6d1259c1739fef0711b11ffa692bcaed3e40a5a41c840"
|
|
5
|
+
"sha256": "f6c72e8e6b48386c23b6d1259c1739fef0711b11ffa692bcaed3e40a5a41c840",
|
|
6
|
+
"js": {
|
|
7
|
+
"binaryen": "132.0.0",
|
|
8
|
+
"octets": 497901,
|
|
9
|
+
"sha256": "a4da12ce262d3c8c86b344fd568bab7e5a63d1a51ea35758606d4f8007f3475b"
|
|
10
|
+
}
|
|
6
11
|
}
|