@anonympins/fingerprint 0.5.1 → 0.5.4
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 +27 -0
- package/README.md +118 -115
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +52 -13
- package/src/js/fingerprint.builder.js +169 -168
- package/src/js/fingerprint.client.js +1271 -1189
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +436 -162
- package/src/js/fingerprint.utils.js +30 -0
- package/src/js/gpu_pow.solver.js +96 -1
- package/src/js/library.js +40 -21
- package/src/js/mongodb-store.js +165 -79
- package/src/js/pow.solver.inline.js +627 -30
- package/src/js/pow.solver.js +453 -31
- package/src/js/problem-manager.js +1 -1
- package/src/js/redis-store.js +114 -42
- package/src/js/tests/dns-circuit-breaker.test.js +55 -0
- package/src/js/tests/fingerprint.client.init.test.js +143 -143
- package/src/js/tests/fingerprint.client.test.js +128 -128
- package/src/js/tests/fingerprint.engine.test.js +370 -370
- package/src/js/tests/fingerprint.test.js +119 -5
- package/src/js/tests/gpu_pow.test.js +81 -0
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/pow.solver.test.js +9 -6
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/Challenge/ChallengeUtils.php +137 -35
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +335 -58
- package/src/php/Optimization/Optimization.php +23 -16
- package/src/php/Optimization/OptimizationOperators.php +27 -0
- package/src/php/ProblemManager.php +1 -1
- package/src/php/Store/MongoDbStore.php +184 -104
- package/src/php/Store/RedisStore.php +91 -11
- package/src/php/Tests/ChallengeUtilsTest.php +285 -285
- package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
- package/src/php/Tests/IpReputationTest.php +1 -1
- package/src/php/Tests/PowTest.php +1 -1
- package/src/php/Tests/RequestUtilsTest.php +29 -0
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/RequestUtils.php +74 -21
- package/src/php/Utils/TLSClientHelloParser.php +212 -116
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
default_whitelist,
|
|
32
32
|
stopThresholdAutoTuning,
|
|
33
33
|
getCompositeDeviceHash,
|
|
34
|
+
sanitizeTrafficData,
|
|
34
35
|
} = fingerprint;
|
|
35
36
|
const { store, getRequestPatternScore, getDeviceHash } = __internal;
|
|
36
37
|
const { getRenderingAnomalyScore } = __internal;
|
|
@@ -749,7 +750,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
749
750
|
pow_type: 'cpu_mem',
|
|
750
751
|
pow_nonce: nonce,
|
|
751
752
|
pow_solution_cpu: String(cpuSolution),
|
|
752
|
-
pow_solution_mem:
|
|
753
|
+
pow_solution_mem: JSON.stringify(memSolution),
|
|
753
754
|
pow_fp: solverFingerprint
|
|
754
755
|
},
|
|
755
756
|
headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint },
|
|
@@ -843,7 +844,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
843
844
|
pow_type: 'cpu_mem',
|
|
844
845
|
pow_nonce: nonce,
|
|
845
846
|
pow_solution_cpu: String(cpuSolution),
|
|
846
|
-
pow_solution_mem:
|
|
847
|
+
pow_solution_mem: JSON.stringify(memSolution),
|
|
847
848
|
pow_fp: solverFingerprint // The client submits its fingerprint.
|
|
848
849
|
},
|
|
849
850
|
headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint }, rawHeaders: ['User-Agent', userAgent], httpVersion: '1.1'
|
|
@@ -1083,6 +1084,35 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1083
1084
|
describe('Threshold Auto-Tuning', () => {
|
|
1084
1085
|
let setIntervalSpy, clearIntervalSpy, consoleLogSpy;
|
|
1085
1086
|
|
|
1087
|
+
test('should sanitize traffic data based on stable hardware cluster', async () => {
|
|
1088
|
+
const trafficData = [];
|
|
1089
|
+
// Attacker rotating IPs and deviceIds but having the same hardware fingerprint
|
|
1090
|
+
for (let i = 0; i < 100; i++) {
|
|
1091
|
+
trafficData.push({
|
|
1092
|
+
type: 'trap_triggered',
|
|
1093
|
+
deviceId: `attacker-dev-${i}`,
|
|
1094
|
+
clientIp: `192.168.1.${i}`,
|
|
1095
|
+
fingerprint: 'gpu:spoofed-nvidia-gpu|cvs:spoofed-canvas-data'
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Unique legit users
|
|
1100
|
+
for (let i = 0; i < 10; i++) {
|
|
1101
|
+
trafficData.push({
|
|
1102
|
+
type: 'challenge_solved',
|
|
1103
|
+
deviceId: `legit-dev-${i}`,
|
|
1104
|
+
clientIp: `10.0.0.${i}`,
|
|
1105
|
+
fingerprint: `gpu:legit-gpu-${i}|cvs:legit-canvas-${i}`
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const sanitized = sanitizeTrafficData(trafficData);
|
|
1110
|
+
const attackerLogs = sanitized.filter(log => log.deviceId.startsWith('attacker-dev-'));
|
|
1111
|
+
|
|
1112
|
+
// Total 110 logs. 2% limit = Math.max(3, 2) = 3.
|
|
1113
|
+
expect(attackerLogs.length).toBeLessThanOrEqual(3);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1086
1116
|
beforeEach(() => {
|
|
1087
1117
|
setIntervalSpy = vi.spyOn(global, 'setInterval');
|
|
1088
1118
|
clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
|
@@ -2122,7 +2152,7 @@ describe('Challenge Page Generation Security (XSS)', () => {
|
|
|
2122
2152
|
});
|
|
2123
2153
|
|
|
2124
2154
|
it('should escape the path parameter in generateCombinedPoWChallengePage to prevent XSS', () => {
|
|
2125
|
-
const maliciousPath = `test.com";\nconsole.log("pwned");//`;
|
|
2155
|
+
const maliciousPath = `test.com";\nconsole.log("pwned");//`; // Original malicious path
|
|
2126
2156
|
const challengeDetails = { nonce: 'test-nonce', target: '0000', path: maliciousPath };
|
|
2127
2157
|
|
|
2128
2158
|
const html = generateCombinedPoWChallengePage(challengeDetails, 16, '127.0.0.1', 'secret', {}, '');
|
|
@@ -2305,9 +2335,9 @@ describe('Subnet Scoring (Node.js)', () => {
|
|
|
2305
2335
|
// The new version handles other prefixes
|
|
2306
2336
|
expect(getIpSubnet('10.20.30.40', 16)).toBe('10.20.0.0/16');
|
|
2307
2337
|
// IPv6
|
|
2308
|
-
expect(getIpSubnet('2001:db8:abcd:0012:0000:0000:0000:0001', 48)).toBe('2001:
|
|
2338
|
+
expect(getIpSubnet('2001:db8:abcd:0012:0000:0000:0000:0001', 48)).toBe('2001:0db8:abcd:0000:0000:0000:0000:0000/48');
|
|
2309
2339
|
// The new version handles other prefixes
|
|
2310
|
-
expect(getIpSubnet('2a01:e0a:129:57c0:a1b2:c3d4:e5f6:a7b8', 64)).toBe('2a01:
|
|
2340
|
+
expect(getIpSubnet('2a01:e0a:129:57c0:a1b2:c3d4:e5f6:a7b8', 64)).toBe('2a01:0e0a:0129:0000:0000:0000:0000:0000/48');
|
|
2311
2341
|
// Invalid IPs
|
|
2312
2342
|
expect(getIpSubnet('not-an-ip')).toBeNull();
|
|
2313
2343
|
});
|
|
@@ -2721,3 +2751,87 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
|
|
|
2721
2751
|
const isDiffIpValid = await fingerprint.isTicketValid('192.168.1.1', ticket, 'device-123', 'hash-abc', false);
|
|
2722
2752
|
expect(isDiffIpValid).toBe(false);
|
|
2723
2753
|
});
|
|
2754
|
+
|
|
2755
|
+
describe('IP Registration and Filtering', () => {
|
|
2756
|
+
const localInMemoryStore = {
|
|
2757
|
+
_map: new Map(),
|
|
2758
|
+
async get(key) { return this._map.get(key); },
|
|
2759
|
+
async set(key, value) { this._map.set(key, value); },
|
|
2760
|
+
async has(key) { return this._map.has(key); },
|
|
2761
|
+
async delete(key) { this._map.delete(key); },
|
|
2762
|
+
clear() { this._map.clear(); }
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
beforeEach(() => {
|
|
2766
|
+
localInMemoryStore.clear();
|
|
2767
|
+
configureStore(localInMemoryStore);
|
|
2768
|
+
vi.restoreAllMocks();
|
|
2769
|
+
vi.spyOn(fingerprint.__internal, 'getTlsFingerprint').mockReturnValue({
|
|
2770
|
+
ja3: 'mock-ja3', ja4: 'mock-ja4'
|
|
2771
|
+
});
|
|
2772
|
+
});
|
|
2773
|
+
|
|
2774
|
+
it('should immediately allow requests from IPs in the static allowlist', async () => {
|
|
2775
|
+
const whitelistedIp = '198.51.100.42';
|
|
2776
|
+
const config = {
|
|
2777
|
+
weights: { historyScore: 1.0 },
|
|
2778
|
+
thresholds: { low: 20, block: 95 },
|
|
2779
|
+
whitelist: [
|
|
2780
|
+
{ type: 'allowlist', entries: [whitelistedIp] }
|
|
2781
|
+
]
|
|
2782
|
+
};
|
|
2783
|
+
const engine = new FingerprintEngine(config);
|
|
2784
|
+
|
|
2785
|
+
const req = {
|
|
2786
|
+
clientIp: whitelistedIp,
|
|
2787
|
+
path: '/',
|
|
2788
|
+
cookies: {},
|
|
2789
|
+
query: {},
|
|
2790
|
+
headers: { 'user-agent': 'test-ua' },
|
|
2791
|
+
rawHeaders: ['User-Agent', 'test-ua'],
|
|
2792
|
+
httpVersion: '1.1'
|
|
2793
|
+
};
|
|
2794
|
+
|
|
2795
|
+
const decision = await engine.processRequest(req);
|
|
2796
|
+
expect(decision.action).toBe('next');
|
|
2797
|
+
expect(decision.vector.whitelisted).toBe(100);
|
|
2798
|
+
expect(decision.vector.type).toBe('allowlist');
|
|
2799
|
+
});
|
|
2800
|
+
|
|
2801
|
+
it('should register distinct client IPs for a device in the data store', async () => {
|
|
2802
|
+
const ip1 = '192.168.1.100';
|
|
2803
|
+
const ip2 = '192.168.1.101';
|
|
2804
|
+
const deviceId = 'test-ip-reg-device';
|
|
2805
|
+
|
|
2806
|
+
await localInMemoryStore.set(`device:${deviceId}`, {
|
|
2807
|
+
initialDeviceHash: 'some-hash',
|
|
2808
|
+
ips: new Set([ip1]),
|
|
2809
|
+
lastUpdate: Date.now(),
|
|
2810
|
+
lastFpHash: 'some-hash',
|
|
2811
|
+
lastChangeTimestamp: 0,
|
|
2812
|
+
rapidChangeCount: 0,
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
const req = {
|
|
2816
|
+
clientIp: ip2,
|
|
2817
|
+
path: '/',
|
|
2818
|
+
cookies: { device_id: deviceId },
|
|
2819
|
+
query: {},
|
|
2820
|
+
headers: { 'user-agent': 'test-ua' },
|
|
2821
|
+
rawHeaders: ['User-Agent', 'test-ua'],
|
|
2822
|
+
httpVersion: '1.1'
|
|
2823
|
+
};
|
|
2824
|
+
|
|
2825
|
+
const engine = new FingerprintEngine({
|
|
2826
|
+
weights: { historyScore: 1.0 },
|
|
2827
|
+
thresholds: { low: 20, block: 95 }
|
|
2828
|
+
});
|
|
2829
|
+
|
|
2830
|
+
await engine.processRequest(req);
|
|
2831
|
+
|
|
2832
|
+
const storedData = await localInMemoryStore.get(`device:${deviceId}`);
|
|
2833
|
+
expect(storedData.ips).toBeInstanceOf(Set);
|
|
2834
|
+
expect(storedData.ips.has(ip1)).toBe(true);
|
|
2835
|
+
expect(storedData.ips.has(ip2)).toBe(true);
|
|
2836
|
+
});
|
|
2837
|
+
});
|
|
@@ -115,4 +115,85 @@ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
|
|
|
115
115
|
expect(result.solution.split(',').length).toBe(64);
|
|
116
116
|
vi.restoreAllMocks();
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
it('should fallback to WebGL1 if both WebGPU and WebGL2 are not supported', async () => {
|
|
120
|
+
// Mock WebGPU as undefined
|
|
121
|
+
Object.defineProperty(global.navigator, 'gpu', {
|
|
122
|
+
value: undefined,
|
|
123
|
+
writable: true,
|
|
124
|
+
configurable: true
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Mock WebGL1 (webgl) context with float extensions enabled
|
|
128
|
+
const mockGl1 = {
|
|
129
|
+
FRAMEBUFFER: 36160,
|
|
130
|
+
FRAMEBUFFER_COMPLETE: 36053,
|
|
131
|
+
COLOR_ATTACHMENT0: 36064,
|
|
132
|
+
TEXTURE_2D: 3553,
|
|
133
|
+
RGBA: 6408,
|
|
134
|
+
FLOAT: 5126,
|
|
135
|
+
TRIANGLES: 4,
|
|
136
|
+
CLAMP_TO_EDGE: 33071,
|
|
137
|
+
TEXTURE_MAG_FILTER: 10240,
|
|
138
|
+
TEXTURE_MIN_FILTER: 10241,
|
|
139
|
+
NEAREST: 9728,
|
|
140
|
+
TEXTURE_WRAP_S: 10242,
|
|
141
|
+
TEXTURE_WRAP_T: 10243,
|
|
142
|
+
useProgram: vi.fn(),
|
|
143
|
+
createBuffer: vi.fn(),
|
|
144
|
+
bindBuffer: vi.fn(),
|
|
145
|
+
bufferData: vi.fn(),
|
|
146
|
+
enableVertexAttribArray: vi.fn(),
|
|
147
|
+
vertexAttribPointer: vi.fn(),
|
|
148
|
+
uniform1f: vi.fn(),
|
|
149
|
+
uniform1i: vi.fn(),
|
|
150
|
+
getUniformLocation: vi.fn(),
|
|
151
|
+
getAttribLocation: vi.fn(),
|
|
152
|
+
drawArrays: vi.fn(),
|
|
153
|
+
getExtension: vi.fn().mockImplementation((ext) => {
|
|
154
|
+
if (ext === 'OES_texture_float' || ext === 'WEBGL_color_buffer_float') {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}),
|
|
159
|
+
createTexture: vi.fn(),
|
|
160
|
+
bindTexture: vi.fn(),
|
|
161
|
+
texImage2D: vi.fn(),
|
|
162
|
+
texParameteri: vi.fn(),
|
|
163
|
+
createFramebuffer: vi.fn(),
|
|
164
|
+
bindFramebuffer: vi.fn(),
|
|
165
|
+
framebufferTexture2D: vi.fn(),
|
|
166
|
+
checkFramebufferStatus: vi.fn().mockReturnValue(36053), // gl.FRAMEBUFFER_COMPLETE
|
|
167
|
+
viewport: vi.fn(),
|
|
168
|
+
readPixels: vi.fn((x, y, w, h, format, type, pixels) => {
|
|
169
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
170
|
+
pixels[i] = 0.75;
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const mockCanvas = {
|
|
176
|
+
width: 8,
|
|
177
|
+
height: 8,
|
|
178
|
+
getContext: vi.fn().mockImplementation((contextId) => {
|
|
179
|
+
if (contextId === 'webgl2') return null;
|
|
180
|
+
if (contextId === 'webgl' || contextId === 'experimental-webgl') return mockGl1;
|
|
181
|
+
return null;
|
|
182
|
+
})
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
|
|
186
|
+
if (tagName === 'canvas') return mockCanvas;
|
|
187
|
+
return {};
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
vi.spyOn(GpuPowSolver, '_createProgram').mockReturnValue({});
|
|
191
|
+
|
|
192
|
+
const result = await GpuPowSolver.solve(seed, 100);
|
|
193
|
+
|
|
194
|
+
expect(result.platform).toBe('webgl1');
|
|
195
|
+
expect(result.solution.split(',').length).toBe(64);
|
|
196
|
+
expect(result.solution.split(',')[0]).toBe('0.750000');
|
|
197
|
+
vi.restoreAllMocks();
|
|
198
|
+
});
|
|
118
199
|
});
|
|
@@ -96,7 +96,7 @@ describe('IP Reputation Local System (Node.js)', () => {
|
|
|
96
96
|
expect(ipv4Subnet).toBe('192.168.1.0/24');
|
|
97
97
|
|
|
98
98
|
const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
|
|
99
|
-
expect(ipv6Subnet).toBe('2001:
|
|
99
|
+
expect(ipv6Subnet).toBe('2001:0db8:abcd:0000:0000:0000:0000:0000/48');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it('should calculate client hints inconsistency score correctly', async () => {
|
|
@@ -63,8 +63,8 @@ describe('Proof-of-Work Solvers', () => {
|
|
|
63
63
|
const solution1 = await solveMemory(seed, difficulty);
|
|
64
64
|
const solution2 = await solveMemory(seed, difficulty);
|
|
65
65
|
|
|
66
|
-
expect(solution1).
|
|
67
|
-
expect(solution1).not.
|
|
66
|
+
expect(solution1).toStrictEqual(solution2);
|
|
67
|
+
expect(solution1).not.toStrictEqual(await solveMemory('different-seed', difficulty));
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -106,7 +106,7 @@ describe('Proof-of-Work Solvers', () => {
|
|
|
106
106
|
|
|
107
107
|
const solutions = await solveChallenge(challenge, ''); // Le fingerprint est passé en 2e arg
|
|
108
108
|
expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
|
|
109
|
-
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(
|
|
109
|
+
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Object));
|
|
110
110
|
}, 20000);
|
|
111
111
|
|
|
112
112
|
it('should solve a "cpu_mem_inline" challenge for a browser', async () => {
|
|
@@ -122,7 +122,7 @@ describe('Proof-of-Work Solvers', () => {
|
|
|
122
122
|
|
|
123
123
|
const solutions = await solveChallenge(challenge, '');
|
|
124
124
|
expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
|
|
125
|
-
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(
|
|
125
|
+
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Object));
|
|
126
126
|
}, 20000);
|
|
127
127
|
|
|
128
128
|
it('should solve a "tsp" challenge', async () => {
|
|
@@ -223,9 +223,12 @@ describe('Proof-of-Work Solvers', () => {
|
|
|
223
223
|
const seed = 'test-wasm-seed';
|
|
224
224
|
const difficulty = 4; // 4 MB
|
|
225
225
|
|
|
226
|
-
const
|
|
226
|
+
const result = await solveMemory(seed, difficulty);
|
|
227
227
|
|
|
228
|
-
expect(
|
|
228
|
+
expect(result).toBeInstanceOf(Object);
|
|
229
|
+
expect(result.solution).toBe(777);
|
|
230
|
+
expect(result.merkleRoot).toBeDefined();
|
|
231
|
+
expect(result.proofs).toBeDefined();
|
|
229
232
|
expect(mockWasmModule._malloc).toHaveBeenCalledTimes(1);
|
|
230
233
|
expect(mockWasmModule._free).toHaveBeenCalledTimes(1);
|
|
231
234
|
expect(mockWasmModule._solve_memory_challenge).toHaveBeenCalledWith(1234, difficulty);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { solveChallenge } from '../pow.solver.js'; // Assuming pow.solver.js is the ES module version
|
|
3
|
+
|
|
4
|
+
// Mock crypto.subtle for hashing operations
|
|
5
|
+
const mockSubtleDigest = vi.fn(async (algorithm, data) => {
|
|
6
|
+
// Return a consistent mock hash for testing purposes
|
|
7
|
+
const hash = new Uint8Array(32); // 32 bytes for SHA-256
|
|
8
|
+
for (let i = 0; i < hash.length; i++) {
|
|
9
|
+
hash[i] = i; // Simple predictable hash
|
|
10
|
+
}
|
|
11
|
+
return hash.buffer;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Mock Math.random for deterministic optimization results
|
|
15
|
+
const mockMathRandom = vi.fn(() => 0.5);
|
|
16
|
+
|
|
17
|
+
// Mock IndexedDB for pospace challenges, even if not directly used by this test
|
|
18
|
+
const mockIndexedDB = {
|
|
19
|
+
open: vi.fn(() => ({
|
|
20
|
+
onupgradeneeded: null,
|
|
21
|
+
onsuccess: null,
|
|
22
|
+
onerror: null,
|
|
23
|
+
result: {
|
|
24
|
+
objectStoreNames: { contains: vi.fn(() => true) },
|
|
25
|
+
createObjectStore: vi.fn(),
|
|
26
|
+
transaction: vi.fn(() => ({
|
|
27
|
+
objectStore: vi.fn(() => ({
|
|
28
|
+
put: vi.fn(),
|
|
29
|
+
get: vi.fn(() => ({ onsuccess: null, result: null }))
|
|
30
|
+
}))
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
33
|
+
}))
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('Useful Work Solver for Optimization Challenges', () => {
|
|
37
|
+
let originalMathRandom;
|
|
38
|
+
let originalIndexedDB;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
// Save original implementations
|
|
42
|
+
originalMathRandom = global.Math.random;
|
|
43
|
+
originalIndexedDB = global.indexedDB;
|
|
44
|
+
|
|
45
|
+
// Mock global objects
|
|
46
|
+
vi.stubGlobal('crypto', {
|
|
47
|
+
subtle: {
|
|
48
|
+
digest: mockSubtleDigest,
|
|
49
|
+
},
|
|
50
|
+
randomInt: vi.fn((min, max) => Math.floor(mockMathRandom() * (max - min) + min)), // Mock randomInt for crypto
|
|
51
|
+
randomBytes: vi.fn((length) => new Uint8Array(length).fill(0x01)), // Mock randomBytes for crypto
|
|
52
|
+
});
|
|
53
|
+
global.Math.random = mockMathRandom;
|
|
54
|
+
global.indexedDB = mockIndexedDB;
|
|
55
|
+
|
|
56
|
+
// Reset mocks
|
|
57
|
+
mockSubtleDigest.mockClear();
|
|
58
|
+
mockMathRandom.mockClear();
|
|
59
|
+
mockIndexedDB.open.mockClear();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
// Restore original implementations
|
|
64
|
+
vi.unstubAllGlobals();
|
|
65
|
+
global.Math.random = originalMathRandom;
|
|
66
|
+
global.indexedDB = originalIndexedDB;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should solve a "useful_work_task" with fraud.solve and return a Pareto front', async () => {
|
|
70
|
+
const mockChallenge = {
|
|
71
|
+
type: 'useful_work_task',
|
|
72
|
+
nonce: 'test-nonce-fraud',
|
|
73
|
+
usefulWorkTask: {
|
|
74
|
+
problemId: 'fraud_detection_challenge',
|
|
75
|
+
task: {
|
|
76
|
+
type: 'multi_objective_genetic_algorithm',
|
|
77
|
+
solverName: 'fraud.solve',
|
|
78
|
+
generations: 5, // Keep generations low for quick test
|
|
79
|
+
populationSize: 10, // Keep population low for quick test
|
|
80
|
+
payload: {
|
|
81
|
+
legitimateClicks: [
|
|
82
|
+
{ fingerprint: 'fp1', timeToClick: 500, clickX: 10, clickY: 10, mouseEntropy: 0.8, scrollEvents: 5 },
|
|
83
|
+
{ fingerprint: 'fp1', timeToClick: 600, clickX: 12, clickY: 12, mouseEntropy: 0.7, scrollEvents: 6 },
|
|
84
|
+
{ fingerprint: 'fp2', timeToClick: 700, clickX: 20, clickY: 20, mouseEntropy: 0.9, scrollEvents: 7 },
|
|
85
|
+
],
|
|
86
|
+
fraudulentClicks: [
|
|
87
|
+
{ fingerprint: 'fp3', timeToClick: 50, clickX: 5, clickY: 5, mouseEntropy: 0.1, scrollEvents: 0 },
|
|
88
|
+
{ fingerprint: 'fp3', timeToClick: 60, clickX: 6, clickY: 6, mouseEntropy: 0.2, scrollEvents: 1 },
|
|
89
|
+
{ fingerprint: 'fp4', timeToClick: 100, clickX: 10, clickY: 10, mouseEntropy: 0.3, scrollEvents: 2 },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const solution = await solveChallenge(mockChallenge);
|
|
97
|
+
|
|
98
|
+
// Assert the structure of the returned solution
|
|
99
|
+
expect(solution.type).toBe('useful_work_task');
|
|
100
|
+
expect(solution.nonce).toBe('test-nonce-fraud');
|
|
101
|
+
expect(solution.rawSolution).toHaveProperty('problem_id', 'fraud_detection_challenge');
|
|
102
|
+
expect(solution.rawSolution).toHaveProperty('work_result');
|
|
103
|
+
|
|
104
|
+
const workResult = solution.rawSolution.work_result;
|
|
105
|
+
expect(workResult).toHaveProperty('paretoFront');
|
|
106
|
+
expect(workResult.paretoFront).toBeInstanceOf(Array);
|
|
107
|
+
expect(workResult.paretoFront.length).toBeGreaterThan(0);
|
|
108
|
+
|
|
109
|
+
// Check the structure of a single solution in the Pareto front
|
|
110
|
+
const firstParetoSolution = workResult.paretoFront[0];
|
|
111
|
+
expect(firstParetoSolution).toHaveProperty('solution');
|
|
112
|
+
expect(firstParetoSolution.solution).toBeInstanceOf(Array); // fraud.solve returns an array of thresholds
|
|
113
|
+
expect(firstParetoSolution.solution.length).toBe(4); // [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
|
|
114
|
+
expect(firstParetoSolution).toHaveProperty('objectives');
|
|
115
|
+
expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [objective1, objective2]
|
|
116
|
+
expect(firstParetoSolution.objectives.length).toBe(2);
|
|
117
|
+
}, 10000); // Increase timeout for GA, even with low generations/population
|
|
118
|
+
|
|
119
|
+
it('should solve a "useful_work_task" with cpc.solve and return a Pareto front', async () => {
|
|
120
|
+
const mockChallenge = {
|
|
121
|
+
type: 'useful_work_task',
|
|
122
|
+
nonce: 'test-nonce-cpc',
|
|
123
|
+
usefulWorkTask: {
|
|
124
|
+
problemId: 'cpc_optimization_challenge',
|
|
125
|
+
task: {
|
|
126
|
+
type: 'multi_objective_genetic_algorithm',
|
|
127
|
+
solverName: 'cpc.solve',
|
|
128
|
+
generations: 5, // Keep generations low for quick test
|
|
129
|
+
populationSize: 10, // Keep population low for quick test
|
|
130
|
+
payload: {
|
|
131
|
+
platformParams: { optimalBaseCommission: 0.3, optimalBonusFactor: 0.1 },
|
|
132
|
+
website: { relevanceScore: 70 },
|
|
133
|
+
advertiser: { credits: 1000 },
|
|
134
|
+
estimatedImpressions: 500,
|
|
135
|
+
competingAds: [{ id: 'ad1' }, { id: 'ad2' }],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const solution = await solveChallenge(mockChallenge);
|
|
142
|
+
|
|
143
|
+
// Assert the structure of the returned solution
|
|
144
|
+
expect(solution.type).toBe('useful_work_task');
|
|
145
|
+
expect(solution.nonce).toBe('test-nonce-cpc');
|
|
146
|
+
expect(solution.rawSolution).toHaveProperty('problem_id', 'cpc_optimization_challenge');
|
|
147
|
+
expect(solution.rawSolution).toHaveProperty('work_result');
|
|
148
|
+
|
|
149
|
+
const workResult = solution.rawSolution.work_result;
|
|
150
|
+
expect(workResult).toHaveProperty('paretoFront');
|
|
151
|
+
expect(workResult.paretoFront).toBeInstanceOf(Array);
|
|
152
|
+
expect(workResult.paretoFront.length).toBeGreaterThan(0);
|
|
153
|
+
|
|
154
|
+
// Check the structure of a single solution in the Pareto front
|
|
155
|
+
const firstParetoSolution = workResult.paretoFront[0];
|
|
156
|
+
expect(firstParetoSolution).toHaveProperty('solution');
|
|
157
|
+
expect(typeof firstParetoSolution.solution).toBe('number'); // cpc.solve returns a single CPC value
|
|
158
|
+
expect(firstParetoSolution).toHaveProperty('objectives');
|
|
159
|
+
expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [platformRevenue, advertiserValue, marketImbalance]
|
|
160
|
+
expect(firstParetoSolution.objectives.length).toBe(3);
|
|
161
|
+
}, 10000);
|
|
162
|
+
});
|