@anonympins/fingerprint 0.1.4 → 0.2.1
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 +100 -36
- package/fingerprint.builder.js +160 -103
- package/fingerprint.client.js +21 -4
- package/fingerprint.js +491 -295
- package/library.js +25 -19
- package/optimization.worker.js +28 -0
- package/package.json +4 -1
- package/pow.solver.js +205 -22
- package/pow.worker.js +27 -0
- package/problem-manager.js +175 -0
package/library.js
CHANGED
|
@@ -289,20 +289,23 @@ const Optimization = {
|
|
|
289
289
|
* @param {number} numCycles - Le nombre total de cycles à exécuter.
|
|
290
290
|
* @param {boolean} [logProgress=false] - Si true, affiche la progression dans la console.
|
|
291
291
|
* @param {object} [options={}] - Options pour la parallélisation.
|
|
292
|
-
* @param {number} [options.concurrency] - Le nombre de workers à utiliser en parallèle. Par défaut, le nombre de cœurs CPU.
|
|
292
|
+
* @param {number} [options.concurrency] - Le nombre de workers à utiliser en parallèle. Par défaut, le nombre de cœurs CPU.
|
|
293
|
+
* @param {function(number): Array<any>} [options.workerDataGenerator] - Une fonction qui, pour chaque cycle (index), génère les arguments spécifiques à passer au solveur. Si non fournie, `baseSolverArgs` est utilisé tel quel.
|
|
293
294
|
* @returns {Promise<{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}>} Le meilleur résultat et des statistiques.
|
|
294
295
|
*/
|
|
295
296
|
async runMultipleParallel(
|
|
296
297
|
solverName,
|
|
297
|
-
baseSolverArgs,
|
|
298
|
+
baseSolverArgs = [], // Default to empty array if not provided
|
|
298
299
|
numCycles,
|
|
299
300
|
logProgress = false,
|
|
300
301
|
options = {},
|
|
301
302
|
) {
|
|
302
303
|
const concurrency = options.concurrency || os.cpus().length;
|
|
304
|
+
const workerDataGenerator = options.workerDataGenerator;
|
|
305
|
+
|
|
303
306
|
if (logProgress) {
|
|
304
307
|
console.log(
|
|
305
|
-
` (Utilisation d'un pool de ${concurrency} workers pour ${numCycles} cycles)`,
|
|
308
|
+
` (Utilisation d'un pool de ${concurrency} workers pour ${numCycles} cycles avec le solveur ${solverName})`,
|
|
306
309
|
);
|
|
307
310
|
}
|
|
308
311
|
|
|
@@ -317,18 +320,15 @@ const Optimization = {
|
|
|
317
320
|
|
|
318
321
|
const workerData = {
|
|
319
322
|
solverName,
|
|
320
|
-
solverArgs:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
x: Math.random() * 100,
|
|
324
|
-
y: Math.random() * 100,
|
|
325
|
-
})),
|
|
326
|
-
...baseSolverArgs,
|
|
327
|
-
],
|
|
323
|
+
solverArgs: workerDataGenerator
|
|
324
|
+
? workerDataGenerator(taskIndex)
|
|
325
|
+
: baseSolverArgs,
|
|
328
326
|
};
|
|
329
327
|
|
|
330
|
-
const result = await new Promise((resolve, reject) => {
|
|
331
|
-
|
|
328
|
+
const result = await new Promise(async (resolve, reject) => {
|
|
329
|
+
// Le chemin du worker doit être absolu ou relatif au fichier appelant.
|
|
330
|
+
// On utilise import.meta.url pour résoudre le chemin de manière fiable.
|
|
331
|
+
const worker = new Worker(new URL('./optimization.worker.js', import.meta.url), { workerData });
|
|
332
332
|
worker.on("message", resolve);
|
|
333
333
|
worker.on("error", reject);
|
|
334
334
|
worker.on("exit", (code) => {
|
|
@@ -1313,15 +1313,21 @@ Optimization.Operators.createOptimalTtlEvaluator = ({ suspicionScore }) => {
|
|
|
1313
1313
|
* @param {string} numberString - Une chaîne de chiffres (ex: "123456789").
|
|
1314
1314
|
* @returns {number} Un score de déviation (0 = parfait, > 0.15 = suspect).
|
|
1315
1315
|
*/
|
|
1316
|
-
Optimization.Operators.benfordTest = (
|
|
1317
|
-
|
|
1318
|
-
|
|
1316
|
+
Optimization.Operators.benfordTest = (numbers) => {
|
|
1317
|
+
if (!Array.isArray(numbers)) {
|
|
1318
|
+
// Si l'entrée n'est pas un tableau, on ne peut pas l'analyser.
|
|
1319
|
+
return 0;
|
|
1320
|
+
}
|
|
1321
|
+
const leadingDigits = numbers.map(n => String(n).trim().charAt(0))
|
|
1322
|
+
.filter(d => d >= '1' && d <= '9'); // On ne garde que les chiffres de 1 à 9.
|
|
1323
|
+
|
|
1324
|
+
if (leadingDigits.length < 10) {
|
|
1319
1325
|
return 0; // Pas assez de données pour un test fiable
|
|
1320
1326
|
}
|
|
1321
1327
|
|
|
1322
1328
|
const counts = Array(10).fill(0);
|
|
1323
|
-
for (let i = 0; i <
|
|
1324
|
-
counts[parseInt(
|
|
1329
|
+
for (let i = 0; i < leadingDigits.length; i++) {
|
|
1330
|
+
counts[parseInt(leadingDigits[i], 10)]++;
|
|
1325
1331
|
}
|
|
1326
1332
|
|
|
1327
1333
|
// Distribution attendue selon la loi de Benford pour le premier chiffre
|
|
@@ -1332,7 +1338,7 @@ Optimization.Operators.benfordTest = (numberString) => {
|
|
|
1332
1338
|
|
|
1333
1339
|
let totalDeviation = 0;
|
|
1334
1340
|
for (let i = 1; i <= 9; i++) {
|
|
1335
|
-
const observedFrequency = (counts[i] /
|
|
1341
|
+
const observedFrequency = (counts[i] / leadingDigits.length) * 100;
|
|
1336
1342
|
const expectedFrequency = benfordDistribution[i];
|
|
1337
1343
|
totalDeviation += Math.pow(observedFrequency - expectedFrequency, 2);
|
|
1338
1344
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file @/optimization.worker.js
|
|
3
|
+
* @description Web Worker générique pour exécuter les algorithmes d'optimisation de la bibliothèque `Optimization`.
|
|
4
|
+
* Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
8
|
+
import { Optimization } from './library.js'; // Assurez-vous que le chemin est correct
|
|
9
|
+
|
|
10
|
+
if (parentPort) {
|
|
11
|
+
parentPort.on('message', async () => { // Le message est vide, on utilise workerData
|
|
12
|
+
const { solverName, solverArgs } = workerData;
|
|
13
|
+
|
|
14
|
+
// Gérer les solveurs imbriqués (ex: 'Operators.solvePortfolio')
|
|
15
|
+
const solverFunction = solverName.split('.').reduce((obj, prop) => obj && obj[prop], Optimization);
|
|
16
|
+
|
|
17
|
+
if (typeof solverFunction === 'function') {
|
|
18
|
+
try {
|
|
19
|
+
const result = await solverFunction(...solverArgs);
|
|
20
|
+
parentPort.postMessage(result);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
parentPort.postMessage({ error: error.message, stack: error.stack });
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
parentPort.postMessage({ error: `Solver '${solverName}' not found or is not a function in Optimization library.` });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
5
|
"main": "fingerprint.js",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"fingerprint.client.js",
|
|
16
16
|
"fingerprint.builder.js",
|
|
17
17
|
"pow.solver.js",
|
|
18
|
+
"pow.worker.js",
|
|
19
|
+
"problem-manager.js",
|
|
20
|
+
"optimization.worker.js",
|
|
18
21
|
"library.js",
|
|
19
22
|
"redis-store.js",
|
|
20
23
|
"mongodb-store.js",
|
package/pow.solver.js
CHANGED
|
@@ -1,32 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file @/pow.solver.js
|
|
3
3
|
* @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
|
|
4
|
+
* IMPORTANT: Pour les tâches d'optimisation, ce fichier a besoin d'accéder aux algorithmes de `library.js`.
|
|
5
|
+
* Dans un vrai projet, il faudrait bundler une version client de `library.js` et l'importer ici.
|
|
6
|
+
* Pour cet exemple, nous allons copier/coller les fonctions nécessaires.
|
|
4
7
|
* Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
'use strict';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
|
-
* Résout un challenge CPU basé sur une cible
|
|
11
|
-
* @param {
|
|
12
|
-
* @param {string} nonce - Le nonce du challenge.
|
|
13
|
+
* Résout un challenge CPU basé sur une cible en utilisant un bloc de base binaire.
|
|
14
|
+
* @param {Uint8Array} baseBlock - Le bloc de données initial (nonce, secret, fp) fourni par le serveur.
|
|
13
15
|
* @param {bigint} target - La cible à atteindre.
|
|
14
|
-
* @param {string} clientSecret - Le secret client (optionnel).
|
|
15
16
|
* @param {Function} progressCallback - Callback pour les mises à jour de progression.
|
|
16
17
|
* @returns {Promise<number>} La solution (un nombre entier).
|
|
17
18
|
*/
|
|
18
|
-
export async function solveCpuTargetInline(
|
|
19
|
-
//
|
|
20
|
-
|
|
19
|
+
export async function solveCpuTargetInline(baseBlock, target, progressCallback) {
|
|
20
|
+
// --- FIX: Add validation for the target to prevent BigInt conversion errors ---
|
|
21
|
+
if (typeof target !== 'bigint' && (typeof target !== 'string' || !/^[0-9a-fA-F]+$/.test(target))) {
|
|
22
|
+
throw new TypeError(`Invalid target type: expected a BigInt or a hex string, but got ${typeof target} with value ${target}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
26
|
+
// --- END FIX ---
|
|
27
|
+
const encoder = new TextEncoder();
|
|
22
28
|
let cpuSolution = 0;
|
|
23
|
-
|
|
24
|
-
while (true) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
+
|
|
30
|
+
while (true) {
|
|
31
|
+
const solutionBytes = encoder.encode(String(cpuSolution));
|
|
32
|
+
|
|
33
|
+
// Concaténation binaire directe : c'est plus rapide et plus sûr.
|
|
34
|
+
const finalBlock = new Uint8Array(baseBlock.length + solutionBytes.length);
|
|
35
|
+
finalBlock.set(baseBlock);
|
|
36
|
+
finalBlock.set(solutionBytes, baseBlock.length);
|
|
37
|
+
|
|
38
|
+
if (cpuSolution === 0) {
|
|
39
|
+
const reconstructedMsg = new TextDecoder().decode(finalBlock);
|
|
40
|
+
console.log('[FP Solve Debug] Client will hash message:', reconstructedMsg);
|
|
41
|
+
}
|
|
42
|
+
const buf = await crypto.subtle.digest("SHA-256", finalBlock);
|
|
29
43
|
const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
44
|
+
// --- AJOUT DE LOGS POUR LE DÉBOGAGE CÔTÉ CLIENT ---
|
|
45
|
+
if (cpuSolution === 0) { // Log only the first attempt
|
|
46
|
+
console.log(`[FP Client Solve] Attempt 0 hash: "0x${hashHex}"`);
|
|
47
|
+
}
|
|
48
|
+
// --- FIN DES LOGS ---
|
|
30
49
|
if (BigInt('0x' + hashHex) < cpuTarget) break;
|
|
31
50
|
cpuSolution++;
|
|
32
51
|
if (cpuSolution % 100000 === 0) {
|
|
@@ -169,32 +188,186 @@ export async function solveTsp(cities, targetMaxDistance) {
|
|
|
169
188
|
return { path: solutionPath, distance: solutionDistance };
|
|
170
189
|
}
|
|
171
190
|
|
|
191
|
+
// --- Fonctions d'optimisation copiées/adaptées de library.js pour le client ---
|
|
192
|
+
|
|
193
|
+
const ClientOptimizers = {
|
|
194
|
+
simulatedAnnealing(initialSolution, evaluator, neighbor, iterations, temp, cooling) {
|
|
195
|
+
let currentSolution = initialSolution;
|
|
196
|
+
let currentEnergy = evaluator(currentSolution);
|
|
197
|
+
let temperature = temp;
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < iterations; i++) {
|
|
200
|
+
const newSolution = neighbor(currentSolution);
|
|
201
|
+
const newEnergy = evaluator(newSolution);
|
|
202
|
+
if (newEnergy < currentEnergy || Math.random() < Math.exp((currentEnergy - newEnergy) / temperature)) {
|
|
203
|
+
currentSolution = newSolution;
|
|
204
|
+
currentEnergy = newEnergy;
|
|
205
|
+
}
|
|
206
|
+
temperature *= cooling;
|
|
207
|
+
}
|
|
208
|
+
return { solution: currentSolution, energy: currentEnergy };
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
geneticAlgorithm(createIndividual, fitness, crossover, mutate, generations, popSize) {
|
|
212
|
+
let population = Array.from({ length: popSize }, () => {
|
|
213
|
+
const chromosome = createIndividual();
|
|
214
|
+
return { chromosome, fitness: fitness(chromosome) };
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
for (let gen = 0; gen < generations; gen++) {
|
|
218
|
+
population.sort((a, b) => a.fitness - b.fitness);
|
|
219
|
+
const newPopulation = [population[0]]; // Elitism
|
|
220
|
+
while (newPopulation.length < popSize) {
|
|
221
|
+
const p1 = population[Math.floor(Math.random() * (popSize / 2))];
|
|
222
|
+
const p2 = population[Math.floor(Math.random() * (popSize / 2))];
|
|
223
|
+
let offspring = crossover(p1.chromosome, p2.chromosome);
|
|
224
|
+
if (Math.random() < 0.1) offspring = mutate(offspring);
|
|
225
|
+
newPopulation.push({ chromosome: offspring, fitness: fitness(offspring) });
|
|
226
|
+
}
|
|
227
|
+
population = newPopulation;
|
|
228
|
+
}
|
|
229
|
+
return population;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Résout une unité de travail utile (Useful Work Unit).
|
|
235
|
+
* @param {object} task - La tâche envoyée par le serveur.
|
|
236
|
+
* @returns {Promise<object>} Le résultat du calcul.
|
|
237
|
+
*/
|
|
238
|
+
async function solveUsefulWorkTask(task) {
|
|
239
|
+
await new Promise(r => setTimeout(r, 10)); // Yield thread
|
|
240
|
+
|
|
241
|
+
switch (task.type) {
|
|
242
|
+
case 'simulated_annealing_iterations': {
|
|
243
|
+
const { cities } = task.payload;
|
|
244
|
+
const distance = (c1, c2) => Math.sqrt(Math.pow(c1.x - c2.x, 2) + Math.pow(c1.y - c2.y, 2));
|
|
245
|
+
const evaluator = (path) => {
|
|
246
|
+
let total = 0;
|
|
247
|
+
for (let i = 0; i < path.length - 1; i++) total += distance(cities[path[i]], cities[path[i + 1]]);
|
|
248
|
+
total += distance(cities[path[path.length - 1]], cities[path[0]]);
|
|
249
|
+
return total;
|
|
250
|
+
};
|
|
251
|
+
const neighbor = (path) => {
|
|
252
|
+
const newPath = [...path];
|
|
253
|
+
const [i, j] = [Math.floor(Math.random() * path.length), Math.floor(Math.random() * path.length)];
|
|
254
|
+
[newPath[i], newPath[j]] = [newPath[j], newPath[i]];
|
|
255
|
+
return newPath;
|
|
256
|
+
};
|
|
257
|
+
const initialSolution = task.initialSolution || Array.from({ length: cities.length }, (_, i) => i).sort(() => 0.5 - Math.random());
|
|
258
|
+
|
|
259
|
+
return ClientOptimizers.simulatedAnnealing(initialSolution, evaluator, neighbor, task.iterations, task.payload.options.initialTemperature, task.payload.options.coolingRate);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case 'genetic_algorithm_generations': {
|
|
263
|
+
const { assets, maxVolatility } = task.payload;
|
|
264
|
+
const fitness = (weights) => {
|
|
265
|
+
const total = weights.reduce((s, w) => s + w, 0);
|
|
266
|
+
if (total === 0) return Infinity;
|
|
267
|
+
const normW = weights.map(w => w / total);
|
|
268
|
+
const ret = normW.reduce((s, w, i) => s + w * assets[i].expectedReturn, 0);
|
|
269
|
+
const vol = normW.reduce((s, w, i) => s + w * assets[i].volatility, 0);
|
|
270
|
+
if (vol > maxVolatility) return 1000 + (vol - maxVolatility);
|
|
271
|
+
return -ret;
|
|
272
|
+
};
|
|
273
|
+
const createIndividual = () => Array.from({ length: assets.length }, Math.random);
|
|
274
|
+
const crossover = (p1, p2) => p1.map((w, i) => (w + p2[i]) / 2);
|
|
275
|
+
const mutate = p => { const n = [...p], i = Math.floor(Math.random() * n.length); n[i] += (Math.random() - 0.5) * 0.2; return n.map(v => Math.max(0, v)); };
|
|
276
|
+
|
|
277
|
+
// Le client doit recréer la population si elle n'est pas fournie
|
|
278
|
+
const initialPopulation = task.initialPopulation || Array.from({ length: task.payload.options.populationSize }, () => ({ chromosome: createIndividual(), fitness: 0 }));
|
|
279
|
+
initialPopulation.forEach(p => p.fitness = fitness(p.chromosome));
|
|
280
|
+
|
|
281
|
+
const finalPopulation = ClientOptimizers.geneticAlgorithm(createIndividual, fitness, crossover, mutate, task.generations, task.payload.options.populationSize);
|
|
282
|
+
return { population: finalPopulation };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'run_multiple_parallel':
|
|
286
|
+
// Côté client, on ne peut pas utiliser de vrais workers pour `runMultipleParallel`.
|
|
287
|
+
// On exécute donc une version simplifiée : un seul cycle du solveur demandé.
|
|
288
|
+
// Cela reste un travail coûteux et valide le principe du "Useful Work".
|
|
289
|
+
const { solverName, baseSolverArgs } = task;
|
|
290
|
+
const clientSolver = ClientOptimizers[solverName];
|
|
291
|
+
if (!clientSolver) throw new Error(`Solver ${solverName} not found on client.`);
|
|
292
|
+
|
|
293
|
+
// On simule l'appel avec les arguments de base.
|
|
294
|
+
// Note: `baseSolverArgs` peut contenir des options.
|
|
295
|
+
return clientSolver(...baseSolverArgs);
|
|
296
|
+
|
|
297
|
+
default:
|
|
298
|
+
throw new Error(`Unknown useful work type: ${task.type}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Résout une tâche d'optimisation basée sur un algorithme génétique.
|
|
304
|
+
* Reçoit une population et la fait évoluer pendant un certain nombre de générations.
|
|
305
|
+
* NOTE: Cette fonction est une version simplifiée de l'AG de `library.js` adaptée au client.
|
|
306
|
+
* @param {Array<object>} initialPopulation - La population de départ.
|
|
307
|
+
* @param {number} generations - Le nombre de générations à exécuter.
|
|
308
|
+
* @returns {Promise<Array<object>>} La population finale après évolution.
|
|
309
|
+
*/
|
|
310
|
+
export async function solveOptimizationTask(initialPopulation, generations) {
|
|
311
|
+
// Fonctions AG simplifiées (croisement, mutation)
|
|
312
|
+
const crossover = (p1, p2) => p1.map((w, i) => (w + p2[i]) / 2);
|
|
313
|
+
const mutate = (p) => {
|
|
314
|
+
const newP = [...p];
|
|
315
|
+
const i = Math.floor(Math.random() * newP.length);
|
|
316
|
+
newP[i] += (Math.random() - 0.5) * 0.2;
|
|
317
|
+
return newP;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
let population = initialPopulation;
|
|
321
|
+
|
|
322
|
+
for (let gen = 0; gen < generations; gen++) {
|
|
323
|
+
// Sélection simple : on garde les 50% meilleurs
|
|
324
|
+
const parents = population.sort((a, b) => a.fitness - b.fitness).slice(0, Math.ceil(population.length / 2));
|
|
325
|
+
const newPopulation = [...parents]; // Élitisme
|
|
326
|
+
|
|
327
|
+
while (newPopulation.length < population.length) {
|
|
328
|
+
const parent1 = parents[Math.floor(Math.random() * parents.length)];
|
|
329
|
+
const parent2 = parents[Math.floor(Math.random() * parents.length)];
|
|
330
|
+
let offspring = crossover(parent1.chromosome, parent2.chromosome);
|
|
331
|
+
if (Math.random() < 0.1) offspring = mutate(offspring);
|
|
332
|
+
// La fitness sera recalculée côté serveur pour la vérification.
|
|
333
|
+
newPopulation.push({ chromosome: offspring, fitness: -1 });
|
|
334
|
+
}
|
|
335
|
+
population = newPopulation;
|
|
336
|
+
// Pause pour ne pas geler l'UI sur les longues tâches
|
|
337
|
+
if (gen % 10 === 0) await new Promise(r => setTimeout(r, 0));
|
|
338
|
+
}
|
|
339
|
+
return population;
|
|
340
|
+
}
|
|
341
|
+
|
|
172
342
|
/**
|
|
173
343
|
* Fonction principale qui reçoit un objet challenge et le résout.
|
|
174
344
|
* @param {object} challenge - L'objet challenge reçu du serveur.
|
|
175
345
|
* @returns {Promise<object>} Un objet contenant la ou les solutions.
|
|
176
346
|
*/
|
|
177
|
-
export async function solveChallenge(challenge) {
|
|
178
|
-
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
|
|
347
|
+
export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
|
|
348
|
+
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
|
|
179
349
|
const solutions = {};
|
|
180
350
|
|
|
181
351
|
switch (type) {
|
|
182
352
|
case 'cpu_target':
|
|
183
|
-
|
|
353
|
+
// Note: This case is not fully exercised by tests as it relies on Web Workers.
|
|
184
354
|
if (!cpuTarget) {
|
|
185
355
|
throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
186
356
|
}
|
|
187
357
|
const target = cpuTarget; // Keep variable name for consistency below
|
|
188
|
-
|
|
358
|
+
// Pour ce challenge simple, le baseBlock est juste le nonce.
|
|
359
|
+
const baseBlockBytes = new TextEncoder().encode(nonce + ":");
|
|
360
|
+
solutions.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
|
|
189
361
|
break;
|
|
190
362
|
case 'cpu_mem':
|
|
191
363
|
// Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
|
|
192
|
-
const baseMessageCombined =
|
|
364
|
+
const baseMessageCombined = `:${nonce}:${clientSecret}`;
|
|
193
365
|
const memSeed = `:${nonce}:${clientSecret}`;
|
|
194
366
|
const [cpuSol, memSol] = await Promise.all([
|
|
195
367
|
(async () => {
|
|
196
|
-
if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
197
|
-
|
|
368
|
+
if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property."); // Pass fingerprint to solver
|
|
369
|
+
const baseBlock = new Uint8Array(challenge.baseBlock);
|
|
370
|
+
return solveCpuTargetInline(baseBlock, cpuTarget, null);
|
|
198
371
|
})(),
|
|
199
372
|
solveMemory(memSeed, memDifficulty)
|
|
200
373
|
]);
|
|
@@ -203,11 +376,12 @@ export async function solveChallenge(challenge) {
|
|
|
203
376
|
break;
|
|
204
377
|
case 'cpu_mem_inline':
|
|
205
378
|
// Version inline pour compatibilité HTML avec IP incluse
|
|
206
|
-
const memSeedInline =
|
|
379
|
+
const memSeedInline = `:${nonce}:${clientSecret}`;
|
|
207
380
|
const [cpuSolInline, memSolInline] = await Promise.all([
|
|
208
381
|
(async () => {
|
|
209
382
|
if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
210
|
-
|
|
383
|
+
const baseBlock = new Uint8Array(challenge.baseBlock);
|
|
384
|
+
return solveCpuTargetInline(baseBlock, cpuTarget, null);
|
|
211
385
|
})(),
|
|
212
386
|
solveMemory(memSeedInline, memDifficulty)
|
|
213
387
|
]);
|
|
@@ -219,6 +393,15 @@ export async function solveChallenge(challenge) {
|
|
|
219
393
|
solutions.tsp = tspResult.path;
|
|
220
394
|
solutions.distance = tspResult.distance;
|
|
221
395
|
break;
|
|
396
|
+
case 'optimization_task':
|
|
397
|
+
const finalPopulation = await solveOptimizationTask(optimizationTask.population, optimizationTask.generations);
|
|
398
|
+
solutions.population = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
|
|
399
|
+
break;
|
|
400
|
+
case 'useful_work_task':
|
|
401
|
+
const workResult = await solveUsefulWorkTask(usefulWorkTask.task);
|
|
402
|
+
solutions.work_result = workResult;
|
|
403
|
+
solutions.problem_id = usefulWorkTask.problemId;
|
|
404
|
+
break;
|
|
222
405
|
default:
|
|
223
406
|
throw new Error(`Unknown challenge type: ${type}`);
|
|
224
407
|
}
|
package/pow.worker.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file @/pow.worker.js
|
|
3
|
+
* @description Web Worker dédié à la résolution du challenge CPU.
|
|
4
|
+
* Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
self.onmessage = async (event) => {
|
|
8
|
+
const { message, target } = event.data;
|
|
9
|
+
let solution = 0;
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
|
|
12
|
+
// La boucle de calcul intensive est isolée dans ce worker.
|
|
13
|
+
while (true) {
|
|
14
|
+
const currentMessage = `${message}:${solution}`;
|
|
15
|
+
const data = encoder.encode(currentMessage);
|
|
16
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
17
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
18
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
19
|
+
|
|
20
|
+
if (BigInt('0x' + hashHex) < target) {
|
|
21
|
+
// Une fois la solution trouvée, on la renvoie au thread principal.
|
|
22
|
+
self.postMessage({ solution });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
solution++;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { Optimization } from './library.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @namespace ProblemInitializers
|
|
6
|
+
* @description Fonctions pour générer dynamiquement les données d'un problème.
|
|
7
|
+
*/
|
|
8
|
+
const ProblemInitializers = {
|
|
9
|
+
/**
|
|
10
|
+
* Génère un ensemble de points aléatoires pour un problème de TSP.
|
|
11
|
+
* @param {object} params - Les paramètres de génération.
|
|
12
|
+
* @param {number} params.count - Le nombre de points à générer.
|
|
13
|
+
* @param {{x: number, y: number}} [params.bounds={x: 1000, y: 1000}] - Les limites spatiales.
|
|
14
|
+
* @returns {Array<{x: number, y: number}>}
|
|
15
|
+
*/
|
|
16
|
+
'generate:randomPoints': (params) => {
|
|
17
|
+
const { count, bounds = { x: 1000, y: 1000 } } = params;
|
|
18
|
+
if (isNaN(count)) return [];
|
|
19
|
+
return Array.from({ length: count }, () => ({ x: Math.random() * bounds.x, y: Math.random() * bounds.y }));
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Génère un ensemble d'actifs financiers aléatoires pour un problème de portefeuille.
|
|
24
|
+
* @param {object} params - Les paramètres de génération.
|
|
25
|
+
* @param {number} params.count - Le nombre d'actifs à générer.
|
|
26
|
+
* @returns {Array<{expectedReturn: number, volatility: number}>}
|
|
27
|
+
*/
|
|
28
|
+
'generate:randomAssets': (params) => {
|
|
29
|
+
const { count } = params;
|
|
30
|
+
if (isNaN(count)) return [];
|
|
31
|
+
return Array.from({ length: count }, () => ({
|
|
32
|
+
expectedReturn: Math.random() * 0.2,
|
|
33
|
+
volatility: 0.1 + Math.random() * 0.3
|
|
34
|
+
}));
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Crée une fonction qui génère des arguments pour chaque worker de `runMultipleParallel`.
|
|
39
|
+
* Permet de faire varier les paramètres (ex: solution initiale) pour chaque cycle.
|
|
40
|
+
* @param {object} params - Les paramètres de configuration.
|
|
41
|
+
* @param {Array<any>} params.baseArgs - Les arguments de base, communs à tous les workers.
|
|
42
|
+
* @param {object} params.variations - Décrit comment faire varier un argument.
|
|
43
|
+
* @returns {function(number): Array<any>} La fonction `workerDataGenerator`.
|
|
44
|
+
*/
|
|
45
|
+
'generate:parallelArgs': (params) => {
|
|
46
|
+
const { baseArgs, variations } = params;
|
|
47
|
+
return (cycleIndex) => {
|
|
48
|
+
const cycleArgs = [...baseArgs];
|
|
49
|
+
// Pour l'instant, on gère la variation de la solution initiale pour le TSP
|
|
50
|
+
if (variations?.initialSolution === 'random') {
|
|
51
|
+
cycleArgs[0] = cycleArgs[0].sort(() => Math.random() - 0.5);
|
|
52
|
+
}
|
|
53
|
+
return cycleArgs;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
class ProblemManager {
|
|
59
|
+
constructor(configPath) {
|
|
60
|
+
this.configPath = configPath;
|
|
61
|
+
this.problems = this.loadProblems();
|
|
62
|
+
this.currentProblemIndex = 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
loadProblems() {
|
|
66
|
+
try {
|
|
67
|
+
const data = readFileSync(this.configPath, 'utf-8');
|
|
68
|
+
const problems = JSON.parse(data);
|
|
69
|
+
// Initialisation dynamique des problèmes
|
|
70
|
+
for (const problem of problems) { // eslint-disable-line no-unused-vars
|
|
71
|
+
for (const key in problem.payload) {
|
|
72
|
+
const value = problem.payload[key];
|
|
73
|
+
// On cherche une instruction d'initialisation (ex: { "$init": "generate:randomPoints", ... })
|
|
74
|
+
if (typeof value === 'object' && value !== null && value.$init) {
|
|
75
|
+
const initializer = ProblemInitializers[value.$init];
|
|
76
|
+
if (initializer) {
|
|
77
|
+
// On remplace l'objet d'instruction par les données générées.
|
|
78
|
+
problem.payload[key] = initializer(value.params || {});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return problems;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`[ProblemManager] Erreur lors du chargement du fichier de problèmes: ${error.message}`);
|
|
86
|
+
return []; // Retourne un tableau vide en cas d'erreur pour éviter un crash
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
saveProblems() {
|
|
91
|
+
// Note: Dans un vrai scénario, utilisez une base de données pour éviter les race conditions.
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(this.configPath, JSON.stringify(this.problems, null, 2));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`[ProblemManager] Erreur lors de la sauvegarde du fichier de problèmes: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sélectionne un problème et génère une unité de travail.
|
|
101
|
+
* @param {number} suspicionFactor - Le facteur de suspicion pour ajuster la difficulté.
|
|
102
|
+
* @returns {{problemId: string, task: object}|null}
|
|
103
|
+
*/
|
|
104
|
+
dispatchWork(suspicionFactor) {
|
|
105
|
+
if (this.problems.length === 0) return null;
|
|
106
|
+
|
|
107
|
+
const problem = this.problems[this.currentProblemIndex];
|
|
108
|
+
this.currentProblemIndex = (this.currentProblemIndex + 1) % this.problems.length;
|
|
109
|
+
|
|
110
|
+
const task = { type: problem.workUnit.type };
|
|
111
|
+
const { scalingFactor } = problem.workUnit;
|
|
112
|
+
|
|
113
|
+
switch (problem.workUnit.type) {
|
|
114
|
+
case 'simulated_annealing_iterations':
|
|
115
|
+
// Assurer une difficulté minimale pour que le challenge soit significatif
|
|
116
|
+
const baseIterations = Math.max(15000, problem.workUnit.baseIterations || 0);
|
|
117
|
+
task.iterations = scalingFactor
|
|
118
|
+
? Math.floor(baseIterations * Math.pow(scalingFactor, suspicionFactor))
|
|
119
|
+
: Math.floor(baseIterations * (0.5 + suspicionFactor));
|
|
120
|
+
task.payload = problem.payload;
|
|
121
|
+
task.initialSolution = problem.state.bestSolution;
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'genetic_algorithm_generations':
|
|
125
|
+
// Assurer une difficulté minimale pour que le challenge soit significatif
|
|
126
|
+
const baseGenerations = Math.max(50, problem.workUnit.baseGenerations || 0);
|
|
127
|
+
task.generations = scalingFactor
|
|
128
|
+
? Math.floor(baseGenerations * Math.pow(scalingFactor, suspicionFactor))
|
|
129
|
+
: Math.floor(baseGenerations * (0.5 + suspicionFactor));
|
|
130
|
+
task.payload = problem.payload;
|
|
131
|
+
task.initialPopulation = problem.state.population;
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'run_multiple_parallel':
|
|
135
|
+
task.solverName = problem.workUnit.solverName;
|
|
136
|
+
task.numCycles = problem.workUnit.numCycles;
|
|
137
|
+
// Les arguments et le générateur sont dans le payload pour plus de flexibilité
|
|
138
|
+
task.baseSolverArgs = problem.payload.baseSolverArgs;
|
|
139
|
+
task.workerDataGenerator = problem.payload.workerDataGenerator;
|
|
140
|
+
task.logProgress = problem.payload.logProgress || false;
|
|
141
|
+
task.concurrency = problem.payload.concurrency;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { problemId: problem.id, task };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Intègre la solution d'un client dans l'état du problème.
|
|
150
|
+
* @param {string} problemId - L'ID du problème.
|
|
151
|
+
* @param {object} solutionData - La solution renvoyée par le client.
|
|
152
|
+
*/
|
|
153
|
+
integrateSolution(problemId, solutionData) {
|
|
154
|
+
const problem = this.problems.find(p => p.id === problemId);
|
|
155
|
+
if (!problem) return;
|
|
156
|
+
|
|
157
|
+
switch (problem.workUnit.type) {
|
|
158
|
+
case 'simulated_annealing_iterations':
|
|
159
|
+
if (solutionData.energy < (parseFloat(problem.state.bestEnergy) || Infinity)) {
|
|
160
|
+
problem.state.bestSolution = solutionData.solution;
|
|
161
|
+
problem.state.bestEnergy = solutionData.energy;
|
|
162
|
+
console.log(`[ProblemManager] Nouvelle meilleure solution pour ${problemId}: ${solutionData.energy.toFixed(2)}`);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
case 'genetic_algorithm_generations':
|
|
166
|
+
problem.state.population = solutionData.population;
|
|
167
|
+
console.log(`[ProblemManager] Population mise à jour pour ${problemId}.`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
this.saveProblems();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { ProblemManager }; // Export the class for testing
|
|
175
|
+
export const problemManager = new ProblemManager('./problems.config.json');
|