@anonympins/fingerprint 0.7.4 → 0.8.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/CHANGELOG.md +59 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +1784 -1738
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +489 -99
- package/src/js/fingerprint.utils.js +252 -2
- package/src/js/pow.worker.js +2 -2
- package/src/js/tests/cross_parity.test.js +3 -3
- package/src/js/tests/dns-circuit-breaker.test.js +6 -6
- package/src/js/tests/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.init.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +37 -12
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
- package/src/js/tests/library.test.js +94 -95
- package/src/js/tests/obfuscation.test.js +8 -8
- package/src/js/tests/quicFingerprint.test.js +47 -0
- package/src/js/tests/tcpFingerprint.test.js +4 -4
- package/src/js/tests/threatIntel.test.js +40 -1
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +392 -86
- package/src/php/Config/SecurityProfiles.php +79 -61
- package/src/php/DirectFingerprint.php +55 -8
- package/src/php/FingerprintBuilder.php +13 -13
- package/src/php/FingerprintClient.php +6 -6
- package/src/php/FingerprintEngine.php +2122 -1690
- package/src/php/Ja3AnomalyDetector.php +22 -22
- package/src/php/Optimization/FunctionRegistry.php +4 -4
- package/src/php/Optimization/Optimization.php +6 -6
- package/src/php/RequestContext.php +11 -11
- package/src/php/Store/InMemoryStore.php +96 -66
- package/src/php/Store/MongoDbStore.php +2 -2
- package/src/php/Store/RedisStore.php +1 -1
- package/src/php/Store/StoreManager.php +48 -35
- package/src/php/Tests/ChallengeUtilsTest.php +31 -0
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintClientTest.php +70 -70
- package/src/php/Tests/PowTest.php +37 -39
- package/src/php/Tests/QuicFingerprintTest.php +115 -53
- package/src/php/Tests/ThreatIntelTest.php +34 -0
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/BlockList.php +13 -13
- package/src/php/Utils/Logger.php +19 -1
- package/src/php/Utils/MaliciousPatterns.php +6 -6
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +362 -1
- package/src/php/Utils/TLSClientHelloParser.php +385 -369
- package/src/php/WordPress/WpDbStore.php +7 -7
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
- package/src/php/WordPress/package.php +80 -101
- package/src/php/WordPress/readme.txt +21 -5
- package/src/php/bin/auto-tune.php +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import {cyrb53} from "./fingerprint.builder.js";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Verifies a Zero-Knowledge Proof (ZKP) of the Schnorr type.
|
|
8
|
+
* The verification checks that g^s ≡ t * y^c (mod p),
|
|
9
|
+
* where c is a challenge computed by SHA-256 hashing of (g, y, t).
|
|
10
|
+
*
|
|
11
|
+
* @param {string} yStr - The public key y, hex-encoded (without the 0x prefix).
|
|
12
|
+
* @param {string} tStr - The commitment t, hex-encoded (without the 0x prefix).
|
|
13
|
+
* @param {string} sStr - The response s, hex-encoded (without the 0x prefix).
|
|
14
|
+
* @returns {boolean} True if the proof is valid, false otherwise (or on error).
|
|
15
|
+
*/
|
|
3
16
|
export function verifyZkpProof(yStr, tStr, sStr) {
|
|
4
17
|
try {
|
|
5
18
|
const y = BigInt('0x' + yStr);
|
|
@@ -19,6 +32,14 @@ export function verifyZkpProof(yStr, tStr, sStr) {
|
|
|
19
32
|
}
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Serializes a value to JSON while escaping dangerous characters
|
|
37
|
+
* for safe inclusion in HTML/JS (XSS prevention).
|
|
38
|
+
* Escapes <, >, U+2028, and U+2029.
|
|
39
|
+
*
|
|
40
|
+
* @param {*} val - The value to serialize.
|
|
41
|
+
* @returns {string} The escaped JSON string.
|
|
42
|
+
*/
|
|
22
43
|
export function safeJsonStringify(val) {
|
|
23
44
|
return JSON.stringify(val)
|
|
24
45
|
.replace(/</g, '\\u003c')
|
|
@@ -27,6 +48,16 @@ export function safeJsonStringify(val) {
|
|
|
27
48
|
.replace(/\u2029/g, '\\u2029');
|
|
28
49
|
}
|
|
29
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Sanitizes and validates a redirect path to prevent open redirects.
|
|
53
|
+
* - Strips out disallowed characters.
|
|
54
|
+
* - Prevents protocol-relative redirects (//evil.com).
|
|
55
|
+
* - Prevents absolute redirects (http://evil.com).
|
|
56
|
+
* - Ensures the path starts with a single '/'.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} p - The raw redirect path.
|
|
59
|
+
* @returns {string} The sanitized path (or '/' by default).
|
|
60
|
+
*/
|
|
30
61
|
export function sanitizeRedirectPath(p) {
|
|
31
62
|
if (typeof p !== 'string') return '/';
|
|
32
63
|
let sanitized = p.replace(/[^a-zA-Z0-9\/.\-_~%?&=:@+,;]/g, '');
|
|
@@ -49,6 +80,16 @@ export function sanitizeRedirectPath(p) {
|
|
|
49
80
|
}
|
|
50
81
|
return sanitized.replace(/^\/+/g, '/');
|
|
51
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decodes a polymorphic fingerprint by restoring the original keys
|
|
86
|
+
* from a mapping of randomized keys.
|
|
87
|
+
* Expected format: "randKey: value|randKey: value|...".
|
|
88
|
+
*
|
|
89
|
+
* @param {string} fpString - The encoded fingerprint.
|
|
90
|
+
* @param {object} mapping - The mapping object containing `keys` (orig -> rand).
|
|
91
|
+
* @returns {string} The decoded fingerprint with original keys.
|
|
92
|
+
*/
|
|
52
93
|
export function decodePolymorphicFingerprint(fpString, mapping) {
|
|
53
94
|
if (!fpString || !mapping || !mapping.keys) return fpString;
|
|
54
95
|
const reverseKeys = {};
|
|
@@ -67,6 +108,130 @@ export function decodePolymorphicFingerprint(fpString, mapping) {
|
|
|
67
108
|
return mappedParts.join('|');
|
|
68
109
|
}
|
|
69
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Extracts and parses a Private Access Token (PAT / Privacy Pass / RFC 9578).
|
|
113
|
+
* Supports 'Authorization: PrivateToken token="..."' and 'Sec-Private-State-Token'.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} headers - HTTP request headers object.
|
|
116
|
+
* @returns {Array<Uint8Array>} Array of raw binary tokens.
|
|
117
|
+
*/
|
|
118
|
+
export function extractPrivateAccessTokens(headers) {
|
|
119
|
+
if (!headers) return [];
|
|
120
|
+
const tokens = [];
|
|
121
|
+
|
|
122
|
+
const authHeader = headers['authorization'] || headers['Authorization'];
|
|
123
|
+
if (typeof authHeader === 'string' && authHeader.toLowerCase().startsWith('privatetoken ')) {
|
|
124
|
+
const match = authHeader.match(/token=(?:"([^"]+)"|([a-zA-Z0-9_\-+/=]+))/i);
|
|
125
|
+
if (match) {
|
|
126
|
+
const rawB64 = match[1] || match[2];
|
|
127
|
+
try {
|
|
128
|
+
tokens.push(Buffer.from(rawB64.replace(/-/g, '+').replace(/_/g, '/'), 'base64'));
|
|
129
|
+
} catch (e) {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pstHeader = headers['sec-private-state-token'] || headers['Sec-Private-State-Token'];
|
|
134
|
+
if (typeof pstHeader === 'string') {
|
|
135
|
+
const parts = pstHeader.split(',');
|
|
136
|
+
for (const part of parts) {
|
|
137
|
+
const trimmed = part.trim();
|
|
138
|
+
if (trimmed) {
|
|
139
|
+
try {
|
|
140
|
+
tokens.push(Buffer.from(trimmed.replace(/-/g, '+').replace(/_/g, '/'), 'base64'));
|
|
141
|
+
} catch (e) {}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return tokens;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Decodes an RFC 9578 Token structure:
|
|
151
|
+
* uint16_t token_type (2 bytes)
|
|
152
|
+
* uint8_t nonce[32] (32 bytes)
|
|
153
|
+
* uint8_t challenge_digest[32] (32 bytes)
|
|
154
|
+
* uint8_t token_key_id[32] (32 bytes)
|
|
155
|
+
* uint8_t authenticator[Nk] (>= 32 bytes)
|
|
156
|
+
*
|
|
157
|
+
* @param {Uint8Array|Buffer} buffer
|
|
158
|
+
* @returns {object|null}
|
|
159
|
+
*/
|
|
160
|
+
export function parsePrivateAccessToken(buffer) {
|
|
161
|
+
if (!buffer || buffer.length < 98) return null; // 2 + 32 + 32 + 32 = 98 min
|
|
162
|
+
|
|
163
|
+
const view = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
|
164
|
+
const tokenType = view.readUInt16BE(0);
|
|
165
|
+
const nonce = view.subarray(2, 34);
|
|
166
|
+
const challengeDigest = view.subarray(34, 66);
|
|
167
|
+
const tokenKeyId = view.subarray(66, 98);
|
|
168
|
+
const authenticator = view.subarray(98);
|
|
169
|
+
|
|
170
|
+
if (authenticator.length === 0) return null;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
tokenType,
|
|
174
|
+
nonce: nonce.toString('hex'),
|
|
175
|
+
challengeDigest: challengeDigest.toString('hex'),
|
|
176
|
+
tokenKeyId: tokenKeyId.toString('hex'),
|
|
177
|
+
authenticator,
|
|
178
|
+
signedData: view.subarray(0, 98)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validates a decoded Private Access Token signature against configured public keys.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} parsedToken - Output from parsePrivateAccessToken.
|
|
186
|
+
* @param {object} patConfig - Configuration with trustedKeys mapping.
|
|
187
|
+
* @returns {boolean} True if signature is cryptographically valid.
|
|
188
|
+
*/
|
|
189
|
+
export function verifyPatSignature(parsedToken, patConfig = {}) {
|
|
190
|
+
if (!parsedToken) return false;
|
|
191
|
+
const trustedKeys = patConfig.trustedKeys || {};
|
|
192
|
+
const keyId = parsedToken.tokenKeyId;
|
|
193
|
+
const publicKey = trustedKeys[keyId] || trustedKeys[keyId.toLowerCase()] || patConfig.defaultPublicKey;
|
|
194
|
+
|
|
195
|
+
if (!publicKey) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { tokenType, signedData, authenticator } = parsedToken;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Type 0x0001 (Blind RSA 2048) / 0x0002 (Rate-Limited Blind RSA)
|
|
203
|
+
if (tokenType === 0x0001 || tokenType === 0x0002 || tokenType === 1 || tokenType === 2) {
|
|
204
|
+
// Try RSA-PSS with SHA-384 (RFC 9577) first, fallback to SHA-256 and PKCS#1 v1.5
|
|
205
|
+
const algorithms = [
|
|
206
|
+
{ hash: 'sha384', padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 48 },
|
|
207
|
+
{ hash: 'sha256', padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 32 },
|
|
208
|
+
{ hash: 'sha384', padding: crypto.constants.RSA_PKCS1_PADDING },
|
|
209
|
+
{ hash: 'sha256', padding: crypto.constants.RSA_PKCS1_PADDING }
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (const alg of algorithms) {
|
|
213
|
+
try {
|
|
214
|
+
const verifier = crypto.createVerify(alg.hash);
|
|
215
|
+
verifier.update(signedData);
|
|
216
|
+
const isValid = verifier.verify({
|
|
217
|
+
key: publicKey,
|
|
218
|
+
padding: alg.padding,
|
|
219
|
+
saltLength: alg.saltLength
|
|
220
|
+
}, authenticator);
|
|
221
|
+
if (isValid) return true;
|
|
222
|
+
} catch (verifyErr) {}
|
|
223
|
+
}
|
|
224
|
+
} else if (tokenType === 0x0003 || tokenType === 3) {
|
|
225
|
+
// VOPRF / Ed25519 authentication verification
|
|
226
|
+
try {
|
|
227
|
+
return crypto.verify(null, signedData, publicKey, authenticator);
|
|
228
|
+
} catch (voprfErr) {}
|
|
229
|
+
}
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
70
235
|
|
|
71
236
|
/**
|
|
72
237
|
* @private
|
|
@@ -127,6 +292,15 @@ export function parseJa3(ja3String) {
|
|
|
127
292
|
};
|
|
128
293
|
}
|
|
129
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Computes modular exponentiation (base^exponent mod modulus) efficiently
|
|
297
|
+
* using the binary exponentiation method (square-and-multiply).
|
|
298
|
+
*
|
|
299
|
+
* @param {bigint} base - The base.
|
|
300
|
+
* @param {bigint} exponent - The exponent.
|
|
301
|
+
* @param {bigint} modulus - The modulus.
|
|
302
|
+
* @returns {bigint} The result of base^exponent mod modulus.
|
|
303
|
+
*/
|
|
130
304
|
export function modPow(base, exponent, modulus) {
|
|
131
305
|
if (modulus === 1n) return 0n;
|
|
132
306
|
let result = 1n;
|
|
@@ -142,6 +316,14 @@ export function modPow(base, exponent, modulus) {
|
|
|
142
316
|
}
|
|
143
317
|
|
|
144
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Computes a simple hash of the IP network by applying a mask (CIDR prefix).
|
|
321
|
+
* For example, with a prefix of 24, only the first 3 octets are kept.
|
|
322
|
+
*
|
|
323
|
+
* @param {string} ip - The IP address in IPv4 format (e.g. "192.168.1.42").
|
|
324
|
+
* @param {number} [prefix=24] - The network prefix length (in bits).
|
|
325
|
+
* @returns {string|null} A hexadecimal hash of the network, or null if the IP is invalid.
|
|
326
|
+
*/
|
|
145
327
|
export function hashNetwork(ip, prefix = 24) {
|
|
146
328
|
// Network hash (/24 or /16 mask)
|
|
147
329
|
const parts = ip.split('.');
|
|
@@ -157,6 +339,14 @@ export function hashNetwork(ip, prefix = 24) {
|
|
|
157
339
|
}
|
|
158
340
|
return hash.toString(16);
|
|
159
341
|
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Checks whether an IP address is a loopback address (localhost).
|
|
345
|
+
* Handles IPv4, IPv6, and the IPv4-mapped prefix (::ffff:).
|
|
346
|
+
*
|
|
347
|
+
* @param {string} ip - The IP address to test.
|
|
348
|
+
* @returns {boolean} True if the IP is a loopback address, false otherwise.
|
|
349
|
+
*/
|
|
160
350
|
export function isLoopbackIp(ip) {
|
|
161
351
|
if (!ip || typeof ip !== 'string') return true;
|
|
162
352
|
let cleanIp = ip.trim().toLowerCase();
|
|
@@ -164,9 +354,16 @@ export function isLoopbackIp(ip) {
|
|
|
164
354
|
cleanIp = cleanIp.substring(7);
|
|
165
355
|
}
|
|
166
356
|
return cleanIp === '127.0.0.1' || cleanIp === '::1' || cleanIp === 'localhost' ||
|
|
167
|
-
|
|
357
|
+
cleanIp.startsWith('127.') || cleanIp === '0.0.0.0' || cleanIp === '::';
|
|
168
358
|
}
|
|
169
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Normalizes a referer URL by keeping only the protocol and hostname
|
|
362
|
+
* (e.g. "https://example.com").
|
|
363
|
+
*
|
|
364
|
+
* @param {string} referer - The raw referer URL.
|
|
365
|
+
* @returns {string} The normalized URL, or the original value if invalid.
|
|
366
|
+
*/
|
|
170
367
|
export function normalizeReferer(referer) {
|
|
171
368
|
try {
|
|
172
369
|
const url = new URL(referer);
|
|
@@ -176,6 +373,13 @@ export function normalizeReferer(referer) {
|
|
|
176
373
|
}
|
|
177
374
|
}
|
|
178
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Checks whether an IP address belongs to a private range (RFC 1918, loopback,
|
|
378
|
+
* link-local, CGNAT, etc.). Handles both IPv4 and IPv6 formats.
|
|
379
|
+
*
|
|
380
|
+
* @param {string} ip - The IP address to test.
|
|
381
|
+
* @returns {boolean} True if the IP is private, false otherwise.
|
|
382
|
+
*/
|
|
179
383
|
export function isPrivateIp(ip) {
|
|
180
384
|
if (!ip || typeof ip !== 'string') return false;
|
|
181
385
|
let cleanIp = ip.trim().toLowerCase();
|
|
@@ -200,7 +404,16 @@ export function isPrivateIp(ip) {
|
|
|
200
404
|
}
|
|
201
405
|
return false;
|
|
202
406
|
}
|
|
203
|
-
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Basic User-Agent parsing to extract:
|
|
410
|
+
* - the browser (with major version),
|
|
411
|
+
* - the operating system,
|
|
412
|
+
* - the device type (mobile, tablet, desktop).
|
|
413
|
+
*
|
|
414
|
+
* @param {string} ua - The User-Agent string.
|
|
415
|
+
* @returns {object} An object { browser?, os?, device }.
|
|
416
|
+
*/
|
|
204
417
|
export function parseUserAgent(ua) {
|
|
205
418
|
// Basic User-Agent parser
|
|
206
419
|
const result = {};
|
|
@@ -238,4 +451,41 @@ export function parseUserAgent(ua) {
|
|
|
238
451
|
else result.device = 'desktop';
|
|
239
452
|
|
|
240
453
|
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Programmatically generates an Ed25519 key pair in PEM format
|
|
458
|
+
* (PKCS#8 for private key, SPKI for public key) matching OpenSSL CLI:
|
|
459
|
+
* `openssl genpkey -algorithm ed25519 -out issuer-private.pem`
|
|
460
|
+
* `openssl pkey -in issuer-private.pem -pubout -out issuer-public.pem`
|
|
461
|
+
*
|
|
462
|
+
* @param {string} [outDir='./config'] Target directory to store the PEM files.
|
|
463
|
+
* @param {object} [options={}] Optional configuration (e.g. custom file names).
|
|
464
|
+
* @returns {{privateKeyPath: string, publicKeyPath: string, privateKey: string, publicKey: string}}
|
|
465
|
+
*/
|
|
466
|
+
export function generateIssuerPemKeys(outDir = './config', options = {}) {
|
|
467
|
+
const privateKeyName = options.privateKeyName || 'issuer-private.pem';
|
|
468
|
+
const publicKeyName = options.publicKeyName || 'issuer-public.pem';
|
|
469
|
+
|
|
470
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
|
|
471
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
472
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' }
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
if (!fs.existsSync(outDir)) {
|
|
476
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const privateKeyPath = path.join(outDir, privateKeyName);
|
|
480
|
+
const publicKeyPath = path.join(outDir, publicKeyName);
|
|
481
|
+
|
|
482
|
+
fs.writeFileSync(privateKeyPath, privateKey, { mode: 0o600 });
|
|
483
|
+
fs.writeFileSync(publicKeyPath, publicKey, { mode: 0o644 });
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
privateKeyPath,
|
|
487
|
+
publicKeyPath,
|
|
488
|
+
privateKey,
|
|
489
|
+
publicKey
|
|
490
|
+
};
|
|
241
491
|
}
|
package/src/js/pow.worker.js
CHANGED
|
@@ -10,7 +10,7 @@ self.onmessage = async (event) => {
|
|
|
10
10
|
let solution = 0;
|
|
11
11
|
const encoder = new TextEncoder();
|
|
12
12
|
if (baseBlock) {
|
|
13
|
-
//
|
|
13
|
+
// solveCpuTargetInline mode (Uint8Array base block)
|
|
14
14
|
const blockArray = new Uint8Array(baseBlock);
|
|
15
15
|
while (true) {
|
|
16
16
|
const solutionBytes = encoder.encode(String(solution));
|
|
@@ -31,7 +31,7 @@ self.onmessage = async (event) => {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
} else {
|
|
34
|
-
//
|
|
34
|
+
// Standard/legacy solveCpuTarget mode (Message string)
|
|
35
35
|
while (true) {
|
|
36
36
|
const currentMessage = `${message}:${solution}`;
|
|
37
37
|
const data = encoder.encode(currentMessage);
|
|
@@ -12,14 +12,14 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
12
12
|
let wasmModule = null;
|
|
13
13
|
|
|
14
14
|
beforeAll(async () => {
|
|
15
|
-
//
|
|
15
|
+
// Attempt on-the-fly WASM compilation if em++ toolchain is available
|
|
16
16
|
try {
|
|
17
17
|
execSync('node src/js/build-client.js', { stdio: 'ignore' });
|
|
18
18
|
} catch (e) {
|
|
19
|
-
//
|
|
19
|
+
// Ignore if Emscripten is missing in environment
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// Instantiate compiled WASM module if available
|
|
23
23
|
if (fs.existsSync(fpJsPath) && fs.existsSync(fpWasmPath)) {
|
|
24
24
|
try {
|
|
25
25
|
const module = await import(fpJsPath);
|
|
@@ -9,15 +9,15 @@ describe('DNS Circuit Breaker', () => {
|
|
|
9
9
|
dnsCircuitBreaker.failureCount = 0;
|
|
10
10
|
dnsCircuitBreaker.lastStateChange = 0;
|
|
11
11
|
dnsCircuitBreaker.threshold = 5;
|
|
12
|
-
dnsCircuitBreaker.cooldownMs = 1000; // 1s
|
|
12
|
+
dnsCircuitBreaker.cooldownMs = 1000; // 1s cooldown to speed up test execution
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
it('
|
|
15
|
+
it('should start in CLOSED state and allow lookups', () => {
|
|
16
16
|
expect(canAttemptDns()).toBe(true);
|
|
17
17
|
expect(dnsCircuitBreaker.state).toBe('CLOSED');
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it('
|
|
20
|
+
it('should transition to OPEN after reaching failure threshold (5)', () => {
|
|
21
21
|
for (let i = 0; i < 4; i++) {
|
|
22
22
|
recordDnsFailure();
|
|
23
23
|
expect(canAttemptDns()).toBe(true);
|
|
@@ -29,7 +29,7 @@ describe('DNS Circuit Breaker', () => {
|
|
|
29
29
|
expect(dnsCircuitBreaker.state).toBe('OPEN');
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
it('
|
|
32
|
+
it('should transition to HALF-OPEN after cooldown expires', async () => {
|
|
33
33
|
for (let i = 0; i < 5; i++) {
|
|
34
34
|
recordDnsFailure();
|
|
35
35
|
}
|
|
@@ -41,12 +41,12 @@ describe('DNS Circuit Breaker', () => {
|
|
|
41
41
|
expect(dnsCircuitBreaker.state).toBe('HALF-OPEN');
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it('
|
|
44
|
+
it('should reset to CLOSED after a success while in HALF-OPEN', async () => {
|
|
45
45
|
for (let i = 0; i < 5; i++) {
|
|
46
46
|
recordDnsFailure();
|
|
47
47
|
}
|
|
48
48
|
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
49
|
-
expect(canAttemptDns()).toBe(true); //
|
|
49
|
+
expect(canAttemptDns()).toBe(true); // Transition to HALF-OPEN
|
|
50
50
|
|
|
51
51
|
recordDnsSuccess();
|
|
52
52
|
expect(dnsCircuitBreaker.state).toBe('CLOSED');
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { DynamicWasmGenerator } from '../dynamic-wasm.js';
|
|
3
|
-
|
|
4
|
-
// Helper function to decode ULEB128 (for verification)
|
|
5
|
-
function decodeULEB128(bytes) {
|
|
6
|
-
let result = 0;
|
|
7
|
-
let shift = 0;
|
|
8
|
-
for (const byte of bytes) {
|
|
9
|
-
result |= (byte & 0x7f) << shift;
|
|
10
|
-
if (!(byte & 0x80)) {
|
|
11
|
-
break;
|
|
12
|
-
}
|
|
13
|
-
shift += 7;
|
|
14
|
-
}
|
|
15
|
-
return result;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('DynamicWasmGenerator', () => {
|
|
19
|
-
it('should generate a valid WASM module buffer', () => {
|
|
20
|
-
const constants = { seed: 123, multiplier: 33, adder: 7 };
|
|
21
|
-
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
22
|
-
|
|
23
|
-
// Basic check: ensure it's a Buffer and starts with WASM magic number
|
|
24
|
-
expect(wasmBuffer).toBeInstanceOf(Buffer);
|
|
25
|
-
expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
|
|
26
|
-
expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
|
|
27
|
-
|
|
28
|
-
// Attempt to compile and instantiate the module to ensure validity
|
|
29
|
-
let instance;
|
|
30
|
-
try {
|
|
31
|
-
const module = new WebAssembly.Module(wasmBuffer);
|
|
32
|
-
instance = new WebAssembly.Instance(module, {});
|
|
33
|
-
} catch (e) {
|
|
34
|
-
// If compilation/instantiation fails, it's an invalid WASM module
|
|
35
|
-
expect.fail(`Generated WASM module is invalid: ${e.message}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Verify the exported hash function exists
|
|
39
|
-
expect(instance.exports.hash).toBeInstanceOf(Function);
|
|
40
|
-
expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should produce a consistent hash result with the JS fallback', () => {
|
|
44
|
-
const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
|
|
45
|
-
const testString = "hello world";
|
|
46
|
-
|
|
47
|
-
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
48
|
-
const module = new WebAssembly.Module(wasmBuffer);
|
|
49
|
-
const instance = new WebAssembly.Instance(module, {});
|
|
50
|
-
const exports = instance.exports;
|
|
51
|
-
const memory = exports.memory;
|
|
52
|
-
|
|
53
|
-
// Write string to WASM memory
|
|
54
|
-
const encoder = new TextEncoder();
|
|
55
|
-
const bytes = encoder.encode(testString);
|
|
56
|
-
const view = new Uint8Array(memory.buffer);
|
|
57
|
-
view.set(bytes, 0); // Assuming hash function expects string at address 0
|
|
58
|
-
|
|
59
|
-
// Calculate hash using WASM
|
|
60
|
-
const wasmHash = exports.hash(0, bytes.length);
|
|
61
|
-
|
|
62
|
-
// Calculate hash using JS fallback
|
|
63
|
-
const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
|
|
64
|
-
|
|
65
|
-
// The WASM and JS implementations should yield the same result
|
|
66
|
-
expect(wasmHash).toBe(jsHash);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should generate different WASM modules for different constants', () => {
|
|
70
|
-
const constants1 = { seed: 1, multiplier: 2, adder: 3 };
|
|
71
|
-
const constants2 = { seed: 4, multiplier: 5, adder: 6 };
|
|
72
|
-
|
|
73
|
-
const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
|
|
74
|
-
const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
|
|
75
|
-
|
|
76
|
-
expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
|
|
77
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DynamicWasmGenerator } from '../dynamic-wasm.js';
|
|
3
|
+
|
|
4
|
+
// Helper function to decode ULEB128 (for verification)
|
|
5
|
+
function decodeULEB128(bytes) {
|
|
6
|
+
let result = 0;
|
|
7
|
+
let shift = 0;
|
|
8
|
+
for (const byte of bytes) {
|
|
9
|
+
result |= (byte & 0x7f) << shift;
|
|
10
|
+
if (!(byte & 0x80)) {
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
shift += 7;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('DynamicWasmGenerator', () => {
|
|
19
|
+
it('should generate a valid WASM module buffer', () => {
|
|
20
|
+
const constants = { seed: 123, multiplier: 33, adder: 7 };
|
|
21
|
+
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
22
|
+
|
|
23
|
+
// Basic check: ensure it's a Buffer and starts with WASM magic number
|
|
24
|
+
expect(wasmBuffer).toBeInstanceOf(Buffer);
|
|
25
|
+
expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
|
|
26
|
+
expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
|
|
27
|
+
|
|
28
|
+
// Attempt to compile and instantiate the module to ensure validity
|
|
29
|
+
let instance;
|
|
30
|
+
try {
|
|
31
|
+
const module = new WebAssembly.Module(wasmBuffer);
|
|
32
|
+
instance = new WebAssembly.Instance(module, {});
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// If compilation/instantiation fails, it's an invalid WASM module
|
|
35
|
+
expect.fail(`Generated WASM module is invalid: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Verify the exported hash function exists
|
|
39
|
+
expect(instance.exports.hash).toBeInstanceOf(Function);
|
|
40
|
+
expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should produce a consistent hash result with the JS fallback', () => {
|
|
44
|
+
const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
|
|
45
|
+
const testString = "hello world";
|
|
46
|
+
|
|
47
|
+
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
48
|
+
const module = new WebAssembly.Module(wasmBuffer);
|
|
49
|
+
const instance = new WebAssembly.Instance(module, {});
|
|
50
|
+
const exports = instance.exports;
|
|
51
|
+
const memory = exports.memory;
|
|
52
|
+
|
|
53
|
+
// Write string to WASM memory
|
|
54
|
+
const encoder = new TextEncoder();
|
|
55
|
+
const bytes = encoder.encode(testString);
|
|
56
|
+
const view = new Uint8Array(memory.buffer);
|
|
57
|
+
view.set(bytes, 0); // Assuming hash function expects string at address 0
|
|
58
|
+
|
|
59
|
+
// Calculate hash using WASM
|
|
60
|
+
const wasmHash = exports.hash(0, bytes.length);
|
|
61
|
+
|
|
62
|
+
// Calculate hash using JS fallback
|
|
63
|
+
const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
|
|
64
|
+
|
|
65
|
+
// The WASM and JS implementations should yield the same result
|
|
66
|
+
expect(wasmHash).toBe(jsHash);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should generate different WASM modules for different constants', () => {
|
|
70
|
+
const constants1 = { seed: 1, multiplier: 2, adder: 3 };
|
|
71
|
+
const constants2 = { seed: 4, multiplier: 5, adder: 6 };
|
|
72
|
+
|
|
73
|
+
const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
|
|
74
|
+
const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
|
|
75
|
+
|
|
76
|
+
expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
|
|
77
|
+
});
|
|
78
78
|
});
|