mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
fix(deepseek-web): replace unlicensed PoW artifacts (#11732)
Merged via /merge-batch (v3.8.51 provenance sweep). Boarded and validated together with the batch's other provenance/dependency PRs in a combined worktree — full gate suite green. Thank you.
This commit is contained in:
committed by
GitHub
parent
700a735949
commit
657d3a484a
@@ -0,0 +1 @@
|
|||||||
|
- **chore(deepseek-web):** remove the provenance-unresolved DeepSeek PoW WASM binary and its runtime loader/tracing while retaining the existing JavaScript solver (slower at high difficulty) ([#11732](https://github.com/diegosouzapw/OmniRoute/pull/11732)).
|
||||||
@@ -317,11 +317,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"open-sse/lib/deepseek-pow.ts": {
|
|
||||||
"@typescript-eslint/no-explicit-any": {
|
|
||||||
"count": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"open-sse/mcp-server/__tests__/a2aLifecycle.test.ts": {
|
"open-sse/mcp-server/__tests__/a2aLifecycle.test.ts": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
@@ -244,8 +244,8 @@ const nextConfig = {
|
|||||||
"./src/mitm/server.cjs",
|
"./src/mitm/server.cjs",
|
||||||
"./open-sse/services/compression/engines/rtk/filters/**/*.json",
|
"./open-sse/services/compression/engines/rtk/filters/**/*.json",
|
||||||
"./open-sse/services/compression/rules/**/*.json",
|
"./open-sse/services/compression/rules/**/*.json",
|
||||||
"./open-sse/lib/sha3_wasm_bg.wasm",
|
"./open-sse/lib/deepseek-pow-hash.js",
|
||||||
"./open-sse/lib/deepseek-pow-solver.cjs",
|
"./open-sse/lib/deepseek-pow-worker.mjs",
|
||||||
// sql.js WASM is loaded at runtime by the sqljsAdapter fallback tier
|
// sql.js WASM is loaded at runtime by the sqljsAdapter fallback tier
|
||||||
// (better-sqlite3 → node:sqlite → sql.js). Next traces sql-wasm.js but can
|
// (better-sqlite3 → node:sqlite → sql.js). Next traces sql-wasm.js but can
|
||||||
// omit the runtime sql-wasm.wasm asset from the standalone bundle.
|
// omit the runtime sql-wasm.wasm asset from the standalone bundle.
|
||||||
|
|||||||
@@ -141,13 +141,14 @@ function generateFakeCookie(): string {
|
|||||||
|
|
||||||
// ── PoW Solver (DeepSeekHashV1) ─────────────────────────────────────────
|
// ── PoW Solver (DeepSeekHashV1) ─────────────────────────────────────────
|
||||||
|
|
||||||
async function solvePow(challenge: PowChallenge): Promise<string> {
|
async function solvePow(challenge: PowChallenge, signal?: AbortSignal | null): Promise<string> {
|
||||||
const answer = await solveDeepSeekPowAsync(
|
const answer = await solveDeepSeekPowAsync(
|
||||||
challenge.algorithm,
|
challenge.algorithm,
|
||||||
challenge.challenge,
|
challenge.challenge,
|
||||||
challenge.salt,
|
challenge.salt,
|
||||||
challenge.difficulty,
|
challenge.difficulty,
|
||||||
challenge.expire_at
|
challenge.expire_at,
|
||||||
|
{ signal }
|
||||||
);
|
);
|
||||||
if (answer < 0) throw new Error("PoW solver failed");
|
if (answer < 0) throw new Error("PoW solver failed");
|
||||||
return Buffer.from(
|
return Buffer.from(
|
||||||
@@ -566,11 +567,7 @@ export function messagesToPrompt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const effectiveWindow =
|
const effectiveWindow =
|
||||||
historyWindow > 0
|
historyWindow > 0 ? historyWindow : conversation.length > 1 ? DEFAULT_AUTO_HISTORY_WINDOW : 0;
|
||||||
? historyWindow
|
|
||||||
: conversation.length > 1
|
|
||||||
? DEFAULT_AUTO_HISTORY_WINDOW
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (effectiveWindow > 0 && conversation.length > 1) {
|
if (effectiveWindow > 0 && conversation.length > 1) {
|
||||||
// Rolling-window transcript of the most recent turns (#2942, auto-applied per
|
// Rolling-window transcript of the most recent turns (#2942, auto-applied per
|
||||||
@@ -932,7 +929,7 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
|||||||
// One completion attempt against a given session id (fresh PoW per attempt).
|
// One completion attempt against a given session id (fresh PoW per attempt).
|
||||||
const performCompletion = async (sid: string) => {
|
const performCompletion = async (sid: string) => {
|
||||||
const powChallenge = await getPowChallenge(accessToken, signal);
|
const powChallenge = await getPowChallenge(accessToken, signal);
|
||||||
const powAnswer = await solvePow(powChallenge);
|
const powAnswer = await solvePow(powChallenge, signal);
|
||||||
const reqHeaders: Record<string, string> = {
|
const reqHeaders: Record<string, string> = {
|
||||||
...FAKE_HEADERS,
|
...FAKE_HEADERS,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -1145,7 +1142,7 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
|||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
log?.error?.("DEEPSEEK-WEB", `Execute failed: ${msg}`);
|
log?.error?.("DEEPSEEK-WEB", `Execute failed: ${msg}`);
|
||||||
|
|
||||||
if (err instanceof DOMException && err.name === "AbortError") {
|
if (err instanceof Error && err.name === "AbortError") {
|
||||||
return {
|
return {
|
||||||
response: errorResponse(499, "Request cancelled"),
|
response: errorResponse(499, "Request cancelled"),
|
||||||
url: COMPLETION_URL,
|
url: COMPLETION_URL,
|
||||||
|
|||||||
450
open-sse/lib/deepseek-pow-hash.js
Normal file
450
open-sse/lib/deepseek-pow-hash.js
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
// Clean-room reference derived from NIST FIPS 202, Algorithms 1-9. FIPS SHA3-256
|
||||||
|
// uses KECCAK-p[1600,24]. Differential testing against the supplied black-box
|
||||||
|
// vectors identifies DeepSeekHashV1 as the same sponge construction with
|
||||||
|
// KECCAK-p[1600,23] (the last 23 rounds, with round indices 1 through 23).
|
||||||
|
|
||||||
|
const LANE_MASK = (1n << 64n) - 1n;
|
||||||
|
const SHA3_256_RATE_BYTES = 136;
|
||||||
|
const SHA3_DOMAIN_SUFFIX = 0x06;
|
||||||
|
const SHA3_256_OUTPUT_BYTES = 32;
|
||||||
|
|
||||||
|
// Indexed as x + 5*y, matching the FIPS 202 state coordinates.
|
||||||
|
const ROTATION_OFFSETS = [
|
||||||
|
0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14,
|
||||||
|
];
|
||||||
|
|
||||||
|
const ROUND_CONSTANTS = [
|
||||||
|
0x0000000000000001n,
|
||||||
|
0x0000000000008082n,
|
||||||
|
0x800000000000808an,
|
||||||
|
0x8000000080008000n,
|
||||||
|
0x000000000000808bn,
|
||||||
|
0x0000000080000001n,
|
||||||
|
0x8000000080008081n,
|
||||||
|
0x8000000000008009n,
|
||||||
|
0x000000000000008an,
|
||||||
|
0x0000000000000088n,
|
||||||
|
0x0000000080008009n,
|
||||||
|
0x000000008000000an,
|
||||||
|
0x000000008000808bn,
|
||||||
|
0x800000000000008bn,
|
||||||
|
0x8000000000008089n,
|
||||||
|
0x8000000000008003n,
|
||||||
|
0x8000000000008002n,
|
||||||
|
0x8000000000000080n,
|
||||||
|
0x000000000000800an,
|
||||||
|
0x800000008000000an,
|
||||||
|
0x8000000080008081n,
|
||||||
|
0x8000000000008080n,
|
||||||
|
0x0000000080000001n,
|
||||||
|
0x8000000080008008n,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {bigint} value
|
||||||
|
* @param {number} amount
|
||||||
|
* @returns {bigint}
|
||||||
|
*/
|
||||||
|
function rotateLeft64(value, amount) {
|
||||||
|
if (amount === 0) return value;
|
||||||
|
const shift = BigInt(amount);
|
||||||
|
return ((value << shift) | (value >> (64n - shift))) & LANE_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the last `roundCount` rounds of KECCAK-p[1600, roundCount].
|
||||||
|
*
|
||||||
|
* @param {bigint[]} state
|
||||||
|
* @param {number} roundCount
|
||||||
|
*/
|
||||||
|
function keccakP1600Reference(state, roundCount) {
|
||||||
|
/** @type {bigint[]} */
|
||||||
|
const columnParity = new Array(5).fill(0n);
|
||||||
|
/** @type {bigint[]} */
|
||||||
|
const thetaMix = new Array(5).fill(0n);
|
||||||
|
/** @type {bigint[]} */
|
||||||
|
const rhoPiState = new Array(25).fill(0n);
|
||||||
|
const firstRound = ROUND_CONSTANTS.length - roundCount;
|
||||||
|
|
||||||
|
for (let round = firstRound; round < ROUND_CONSTANTS.length; round++) {
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
columnParity[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20];
|
||||||
|
}
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
thetaMix[x] = columnParity[(x + 4) % 5] ^ rotateLeft64(columnParity[(x + 1) % 5], 1);
|
||||||
|
}
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
for (let x = 0; x < 5; x++) state[x + 5 * y] ^= thetaMix[x];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
const destinationX = y;
|
||||||
|
const destinationY = (2 * x + 3 * y) % 5;
|
||||||
|
const lane = x + 5 * y;
|
||||||
|
rhoPiState[destinationX + 5 * destinationY] = rotateLeft64(
|
||||||
|
state[lane],
|
||||||
|
ROTATION_OFFSETS[lane]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
const row = 5 * y;
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
state[x + row] =
|
||||||
|
rhoPiState[x + row] ^
|
||||||
|
(~rhoPiState[((x + 1) % 5) + row] & LANE_MASK & rhoPiState[((x + 2) % 5) + row]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state[0] ^= ROUND_CONSTANTS[round];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {bigint[]} state
|
||||||
|
* @param {Uint8Array} block
|
||||||
|
* @param {number} roundCount
|
||||||
|
*/
|
||||||
|
function absorbReferenceBlock(state, block, roundCount) {
|
||||||
|
for (let index = 0; index < SHA3_256_RATE_BYTES; index++) {
|
||||||
|
const lane = Math.floor(index / 8);
|
||||||
|
const shift = BigInt((index % 8) * 8);
|
||||||
|
state[lane] ^= BigInt(block[index]) << shift;
|
||||||
|
}
|
||||||
|
keccakP1600Reference(state, roundCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SHA3-256's sponge parameters with a selectable KECCAK-p round count.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {number} roundCount
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function sha3_256ReferenceWithRoundCount(input, roundCount) {
|
||||||
|
const bytes = new TextEncoder().encode(input);
|
||||||
|
/** @type {bigint[]} */
|
||||||
|
const state = new Array(25).fill(0n);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
while (offset + SHA3_256_RATE_BYTES <= bytes.length) {
|
||||||
|
absorbReferenceBlock(state, bytes.subarray(offset, offset + SHA3_256_RATE_BYTES), roundCount);
|
||||||
|
offset += SHA3_256_RATE_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalBlock = new Uint8Array(SHA3_256_RATE_BYTES);
|
||||||
|
finalBlock.set(bytes.subarray(offset));
|
||||||
|
finalBlock[bytes.length - offset] ^= SHA3_DOMAIN_SUFFIX;
|
||||||
|
finalBlock[SHA3_256_RATE_BYTES - 1] ^= 0x80;
|
||||||
|
absorbReferenceBlock(state, finalBlock, roundCount);
|
||||||
|
|
||||||
|
const output = new Uint8Array(SHA3_256_OUTPUT_BYTES);
|
||||||
|
for (let index = 0; index < output.length; index++) {
|
||||||
|
const lane = Math.floor(index / 8);
|
||||||
|
const shift = BigInt((index % 8) * 8);
|
||||||
|
output[index] = Number((state[lane] >> shift) & 0xffn);
|
||||||
|
}
|
||||||
|
return Array.from(output, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Readable FIPS 202 control implementation used to validate the permutation,
|
||||||
|
* byte order, padding, and multi-block absorption against `node:crypto`.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function sha3_256Fips202Reference(input) {
|
||||||
|
return sha3_256ReferenceWithRoundCount(input, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Readable DeepSeekHashV1 reference model. Runtime searches use an equivalent
|
||||||
|
* 32-bit implementation below so a bounded synchronous compatibility call does
|
||||||
|
* not turn a 144k challenge into minutes of BigInt work.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function deepSeekHashV1Reference(input) {
|
||||||
|
return sha3_256ReferenceWithRoundCount(input, 23);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEEPSEEK_HASH_ROUNDS = 23;
|
||||||
|
const DIGEST_HEX_PATTERN = /^[a-f0-9]{64}$/i;
|
||||||
|
const ROUND_CONSTANTS_LOW = Uint32Array.from(ROUND_CONSTANTS, (value) =>
|
||||||
|
Number(value & 0xffffffffn)
|
||||||
|
);
|
||||||
|
const ROUND_CONSTANTS_HIGH = Uint32Array.from(ROUND_CONSTANTS, (value) =>
|
||||||
|
Number((value >> 32n) & 0xffffffffn)
|
||||||
|
);
|
||||||
|
const RHO_PI_DESTINATION_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => {
|
||||||
|
const x = lane % 5;
|
||||||
|
const y = Math.floor(lane / 5);
|
||||||
|
return 2 * (y + 5 * ((2 * x + 3 * y) % 5));
|
||||||
|
});
|
||||||
|
const CHI_NEXT_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => {
|
||||||
|
const x = lane % 5;
|
||||||
|
const row = lane - x;
|
||||||
|
return 2 * (row + ((x + 1) % 5));
|
||||||
|
});
|
||||||
|
const CHI_NEXT_NEXT_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => {
|
||||||
|
const x = lane % 5;
|
||||||
|
const row = lane - x;
|
||||||
|
return 2 * (row + ((x + 2) % 5));
|
||||||
|
});
|
||||||
|
const HEX_DIGITS = "0123456789abcdef";
|
||||||
|
|
||||||
|
export const MAX_DEEPSEEK_POW_DIFFICULTY = 250_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent 32-bit form of the reference permutation. Each 64-bit lane is
|
||||||
|
* stored as adjacent little-endian low/high uint32 words.
|
||||||
|
*
|
||||||
|
* @param {Uint32Array} state
|
||||||
|
* @param {Uint32Array} rhoPiState
|
||||||
|
* @param {Uint32Array} columnParity
|
||||||
|
* @param {Uint32Array} thetaMix
|
||||||
|
* @param {number} roundCount
|
||||||
|
*/
|
||||||
|
function keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount) {
|
||||||
|
const firstRound = ROUND_CONSTANTS.length - roundCount;
|
||||||
|
|
||||||
|
for (let round = firstRound; round < ROUND_CONSTANTS.length; round++) {
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
const word = 2 * x;
|
||||||
|
columnParity[word] =
|
||||||
|
state[word] ^ state[word + 10] ^ state[word + 20] ^ state[word + 30] ^ state[word + 40];
|
||||||
|
columnParity[word + 1] =
|
||||||
|
state[word + 1] ^ state[word + 11] ^ state[word + 21] ^ state[word + 31] ^ state[word + 41];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
const previous = 2 * ((x + 4) % 5);
|
||||||
|
const next = 2 * ((x + 1) % 5);
|
||||||
|
const rotatedLow = (columnParity[next] << 1) | (columnParity[next + 1] >>> 31);
|
||||||
|
const rotatedHigh = (columnParity[next + 1] << 1) | (columnParity[next] >>> 31);
|
||||||
|
thetaMix[2 * x] = columnParity[previous] ^ rotatedLow;
|
||||||
|
thetaMix[2 * x + 1] = columnParity[previous + 1] ^ rotatedHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
const word = 2 * x;
|
||||||
|
const low = thetaMix[word];
|
||||||
|
const high = thetaMix[word + 1];
|
||||||
|
state[word] ^= low;
|
||||||
|
state[word + 1] ^= high;
|
||||||
|
state[word + 10] ^= low;
|
||||||
|
state[word + 11] ^= high;
|
||||||
|
state[word + 20] ^= low;
|
||||||
|
state[word + 21] ^= high;
|
||||||
|
state[word + 30] ^= low;
|
||||||
|
state[word + 31] ^= high;
|
||||||
|
state[word + 40] ^= low;
|
||||||
|
state[word + 41] ^= high;
|
||||||
|
}
|
||||||
|
|
||||||
|
rhoPiState[0] = state[0];
|
||||||
|
rhoPiState[1] = state[1];
|
||||||
|
for (let lane = 1; lane < 25; lane++) {
|
||||||
|
const source = 2 * lane;
|
||||||
|
const destination = RHO_PI_DESTINATION_WORDS[lane];
|
||||||
|
const amount = ROTATION_OFFSETS[lane];
|
||||||
|
const low = state[source];
|
||||||
|
const high = state[source + 1];
|
||||||
|
|
||||||
|
if (amount < 32) {
|
||||||
|
rhoPiState[destination] = (low << amount) | (high >>> (32 - amount));
|
||||||
|
rhoPiState[destination + 1] = (high << amount) | (low >>> (32 - amount));
|
||||||
|
} else {
|
||||||
|
const reduced = amount - 32;
|
||||||
|
rhoPiState[destination] = (high << reduced) | (low >>> (32 - reduced));
|
||||||
|
rhoPiState[destination + 1] = (low << reduced) | (high >>> (32 - reduced));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let lane = 0; lane < 25; lane++) {
|
||||||
|
const word = 2 * lane;
|
||||||
|
const nextWord = CHI_NEXT_WORDS[lane];
|
||||||
|
const nextNextWord = CHI_NEXT_NEXT_WORDS[lane];
|
||||||
|
state[word] = rhoPiState[word] ^ (~rhoPiState[nextWord] & rhoPiState[nextNextWord]);
|
||||||
|
state[word + 1] =
|
||||||
|
rhoPiState[word + 1] ^ (~rhoPiState[nextWord + 1] & rhoPiState[nextNextWord + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
state[0] ^= ROUND_CONSTANTS_LOW[round];
|
||||||
|
state[1] ^= ROUND_CONSTANTS_HIGH[round];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint32Array} state
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {number} offset
|
||||||
|
* @param {Uint32Array} rhoPiState
|
||||||
|
* @param {Uint32Array} columnParity
|
||||||
|
* @param {Uint32Array} thetaMix
|
||||||
|
* @param {number} roundCount
|
||||||
|
*/
|
||||||
|
function absorbFullUint32Block(
|
||||||
|
state,
|
||||||
|
bytes,
|
||||||
|
offset,
|
||||||
|
rhoPiState,
|
||||||
|
columnParity,
|
||||||
|
thetaMix,
|
||||||
|
roundCount
|
||||||
|
) {
|
||||||
|
for (let index = 0; index < SHA3_256_RATE_BYTES; index++) {
|
||||||
|
const word = index >>> 2;
|
||||||
|
state[word] ^= bytes[offset + index] << ((index & 3) * 8);
|
||||||
|
}
|
||||||
|
keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint32Array} state
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function digestStateToHex(state) {
|
||||||
|
let digest = "";
|
||||||
|
for (let index = 0; index < SHA3_256_OUTPUT_BYTES; index++) {
|
||||||
|
const byte = (state[index >>> 2] >>> ((index & 3) * 8)) & 0xff;
|
||||||
|
digest += HEX_DIGITS[byte >>> 4] + HEX_DIGITS[byte & 0x0f];
|
||||||
|
}
|
||||||
|
return digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {number} roundCount
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function sha3_256Uint32WithRoundCount(input, roundCount) {
|
||||||
|
const bytes = new TextEncoder().encode(input);
|
||||||
|
const state = new Uint32Array(50);
|
||||||
|
const rhoPiState = new Uint32Array(50);
|
||||||
|
const columnParity = new Uint32Array(10);
|
||||||
|
const thetaMix = new Uint32Array(10);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
while (offset + SHA3_256_RATE_BYTES <= bytes.length) {
|
||||||
|
absorbFullUint32Block(state, bytes, offset, rhoPiState, columnParity, thetaMix, roundCount);
|
||||||
|
offset += SHA3_256_RATE_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = bytes.length - offset;
|
||||||
|
for (let index = 0; index < remaining; index++) {
|
||||||
|
state[index >>> 2] ^= bytes[offset + index] << ((index & 3) * 8);
|
||||||
|
}
|
||||||
|
state[remaining >>> 2] ^= SHA3_DOMAIN_SUFFIX << ((remaining & 3) * 8);
|
||||||
|
state[(SHA3_256_RATE_BYTES - 1) >>> 2] ^= 0x80 << 24;
|
||||||
|
keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount);
|
||||||
|
return digestStateToHex(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized, allocation-bounded DeepSeekHashV1 digest.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function deepSeekHashV1(input) {
|
||||||
|
return sha3_256Uint32WithRoundCount(input, DEEPSEEK_HASH_ROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} digestHex
|
||||||
|
* @returns {Uint32Array}
|
||||||
|
*/
|
||||||
|
function parseDigestWords(digestHex) {
|
||||||
|
const words = new Uint32Array(SHA3_256_OUTPUT_BYTES / 4);
|
||||||
|
for (let index = 0; index < SHA3_256_OUTPUT_BYTES; index++) {
|
||||||
|
const byte = Number.parseInt(digestHex.slice(index * 2, index * 2 + 2), 16);
|
||||||
|
words[index >>> 2] |= byte << ((index & 3) * 8);
|
||||||
|
}
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search `prefix + nonce` without allocating a digest or re-encoding the prefix
|
||||||
|
* for every candidate. Inputs are validated here as well as at the public solver
|
||||||
|
* boundary so the worker cannot be coerced into an unbounded loop.
|
||||||
|
*
|
||||||
|
* @param {string} prefix
|
||||||
|
* @param {string} challenge
|
||||||
|
* @param {number} difficulty
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function findDeepSeekPowNonce(prefix, challenge, difficulty) {
|
||||||
|
if (typeof prefix !== "string") throw new TypeError("DeepSeek PoW prefix must be a string");
|
||||||
|
if (!DIGEST_HEX_PATTERN.test(challenge)) {
|
||||||
|
throw new TypeError("DeepSeek PoW challenge must be a 64-character hex digest");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Number.isSafeInteger(difficulty) ||
|
||||||
|
difficulty < 1 ||
|
||||||
|
difficulty > MAX_DEEPSEEK_POW_DIFFICULTY
|
||||||
|
) {
|
||||||
|
throw new RangeError(
|
||||||
|
`DeepSeek PoW difficulty must be an integer from 1 to ${MAX_DEEPSEEK_POW_DIFFICULTY}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefixBytes = new TextEncoder().encode(prefix);
|
||||||
|
const baseState = new Uint32Array(50);
|
||||||
|
const rhoPiState = new Uint32Array(50);
|
||||||
|
const columnParity = new Uint32Array(10);
|
||||||
|
const thetaMix = new Uint32Array(10);
|
||||||
|
let prefixOffset = 0;
|
||||||
|
|
||||||
|
while (prefixOffset + SHA3_256_RATE_BYTES <= prefixBytes.length) {
|
||||||
|
absorbFullUint32Block(
|
||||||
|
baseState,
|
||||||
|
prefixBytes,
|
||||||
|
prefixOffset,
|
||||||
|
rhoPiState,
|
||||||
|
columnParity,
|
||||||
|
thetaMix,
|
||||||
|
DEEPSEEK_HASH_ROUNDS
|
||||||
|
);
|
||||||
|
prefixOffset += SHA3_256_RATE_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tailLength = prefixBytes.length - prefixOffset;
|
||||||
|
const tailWords = new Uint32Array(Math.ceil(tailLength / 4));
|
||||||
|
for (let index = 0; index < tailLength; index++) {
|
||||||
|
tailWords[index >>> 2] ^= prefixBytes[prefixOffset + index] << ((index & 3) * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetWords = parseDigestWords(challenge.toLowerCase());
|
||||||
|
const state = new Uint32Array(50);
|
||||||
|
|
||||||
|
nonceLoop: for (let nonce = 0; nonce < difficulty; nonce++) {
|
||||||
|
state.set(baseState);
|
||||||
|
for (let word = 0; word < tailWords.length; word++) state[word] ^= tailWords[word];
|
||||||
|
|
||||||
|
let position = tailLength;
|
||||||
|
const nonceText = String(nonce);
|
||||||
|
for (let index = 0; index < nonceText.length; index++) {
|
||||||
|
state[position >>> 2] ^= nonceText.charCodeAt(index) << ((position & 3) * 8);
|
||||||
|
position += 1;
|
||||||
|
if (position === SHA3_256_RATE_BYTES) {
|
||||||
|
keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, DEEPSEEK_HASH_ROUNDS);
|
||||||
|
position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state[position >>> 2] ^= SHA3_DOMAIN_SUFFIX << ((position & 3) * 8);
|
||||||
|
state[(SHA3_256_RATE_BYTES - 1) >>> 2] ^= 0x80 << 24;
|
||||||
|
keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, DEEPSEEK_HASH_ROUNDS);
|
||||||
|
|
||||||
|
for (let word = 0; word < targetWords.length; word++) {
|
||||||
|
if (state[word] !== targetWords[word]) continue nonceLoop;
|
||||||
|
}
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
20
open-sse/lib/deepseek-pow-worker.mjs
Normal file
20
open-sse/lib/deepseek-pow-worker.mjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { parentPort, workerData } from "node:worker_threads";
|
||||||
|
|
||||||
|
import { findDeepSeekPowNonce, MAX_DEEPSEEK_POW_DIFFICULTY } from "./deepseek-pow-hash.js";
|
||||||
|
|
||||||
|
if (!parentPort) {
|
||||||
|
throw new Error("DeepSeek PoW worker requires a parent port");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { challenge, prefix, difficulty } = workerData;
|
||||||
|
if (
|
||||||
|
typeof challenge !== "string" ||
|
||||||
|
typeof prefix !== "string" ||
|
||||||
|
!Number.isSafeInteger(difficulty) ||
|
||||||
|
difficulty < 1 ||
|
||||||
|
difficulty > MAX_DEEPSEEK_POW_DIFFICULTY
|
||||||
|
) {
|
||||||
|
throw new Error("DeepSeek PoW worker received invalid input");
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPort.postMessage(findDeepSeekPowNonce(prefix, challenge, difficulty));
|
||||||
@@ -1,181 +1,156 @@
|
|||||||
import { createRequire } from "node:module";
|
import { existsSync } from "node:fs";
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { Worker } from "node:worker_threads";
|
||||||
|
|
||||||
// ── WASM solver (fast — ~50-100ms at difficulty 144000) ──────────────────
|
import { findDeepSeekPowNonce, MAX_DEEPSEEK_POW_DIFFICULTY } from "./deepseek-pow-hash.js";
|
||||||
|
|
||||||
class DeepSeekHashWasm {
|
const DEEPSEEK_POW_ALGORITHM = "DeepSeekHashV1";
|
||||||
private wasmInstance: any;
|
const SHA3_256_HEX_PATTERN = /^[a-f0-9]{64}$/i;
|
||||||
private offset = 0;
|
const MAX_DIFFICULTY = MAX_DEEPSEEK_POW_DIFFICULTY;
|
||||||
private cachedUint8Memory: Uint8Array | null = null;
|
const MAX_SALT_LENGTH = 1_024;
|
||||||
private cachedTextEncoder = new TextEncoder();
|
const MAX_CONCURRENT_WORKERS = 2;
|
||||||
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||||
|
const MAX_TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
private getCachedUint8Memory(): Uint8Array {
|
let activeWorkerCount = 0;
|
||||||
if (!this.cachedUint8Memory?.byteLength) {
|
|
||||||
this.cachedUint8Memory = new Uint8Array(this.wasmInstance.memory.buffer);
|
|
||||||
}
|
|
||||||
return this.cachedUint8Memory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private encodeString(
|
export interface SolveDeepSeekPowOptions {
|
||||||
text: string,
|
signal?: AbortSignal | null;
|
||||||
allocate: (size: number, align: number) => number,
|
timeoutMs?: number;
|
||||||
reallocate: (ptr: number, oldSize: number, newSize: number, align: number) => number
|
|
||||||
): number {
|
|
||||||
const strLength = text.length;
|
|
||||||
let ptr = allocate(strLength, 1) >>> 0;
|
|
||||||
const memory = this.getCachedUint8Memory();
|
|
||||||
let asciiLength = 0;
|
|
||||||
|
|
||||||
for (; asciiLength < strLength; asciiLength++) {
|
|
||||||
if (text.charCodeAt(asciiLength) > 127) break;
|
|
||||||
memory[ptr + asciiLength] = text.charCodeAt(asciiLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asciiLength !== strLength) {
|
|
||||||
if (asciiLength > 0) text = text.slice(asciiLength);
|
|
||||||
ptr = reallocate(ptr, strLength, asciiLength + text.length * 3, 1) >>> 0;
|
|
||||||
const result = this.cachedTextEncoder.encodeInto(
|
|
||||||
text,
|
|
||||||
this.getCachedUint8Memory().subarray(ptr + asciiLength, ptr + asciiLength + text.length * 3)
|
|
||||||
);
|
|
||||||
asciiLength += result.written!;
|
|
||||||
ptr = reallocate(ptr, asciiLength + text.length * 3, asciiLength, 1) >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.offset = asciiLength;
|
|
||||||
return ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateHash(challenge: string, prefix: string, difficulty: number): number | undefined {
|
|
||||||
try {
|
|
||||||
const retptr = this.wasmInstance.__wbindgen_add_to_stack_pointer(-16);
|
|
||||||
|
|
||||||
const ptr0 = this.encodeString(
|
|
||||||
challenge,
|
|
||||||
this.wasmInstance.__wbindgen_export_0,
|
|
||||||
this.wasmInstance.__wbindgen_export_1
|
|
||||||
);
|
|
||||||
const len0 = this.offset;
|
|
||||||
|
|
||||||
const ptr1 = this.encodeString(
|
|
||||||
prefix,
|
|
||||||
this.wasmInstance.__wbindgen_export_0,
|
|
||||||
this.wasmInstance.__wbindgen_export_1
|
|
||||||
);
|
|
||||||
const len1 = this.offset;
|
|
||||||
|
|
||||||
this.wasmInstance.wasm_solve(retptr, ptr0, len0, ptr1, len1, difficulty);
|
|
||||||
|
|
||||||
const dv = new DataView(this.wasmInstance.memory.buffer);
|
|
||||||
const status = dv.getInt32(retptr + 0, true);
|
|
||||||
const value = dv.getFloat64(retptr + 8, true);
|
|
||||||
|
|
||||||
return status === 0 ? undefined : value;
|
|
||||||
} finally {
|
|
||||||
this.wasmInstance.__wbindgen_add_to_stack_pointer(16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(wasmPath: string): Promise<void> {
|
|
||||||
const wasmBuffer = await fs.promises.readFile(wasmPath);
|
|
||||||
const { instance } = await WebAssembly.instantiate(wasmBuffer, { wbg: {} });
|
|
||||||
this.wasmInstance = instance.exports;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _wasmSolver: DeepSeekHashWasm | null = null;
|
interface ValidatedChallenge {
|
||||||
let _wasmInitFailed = false;
|
challenge: string;
|
||||||
|
prefix: string;
|
||||||
async function getWasmSolver(): Promise<DeepSeekHashWasm | null> {
|
difficulty: number;
|
||||||
if (_wasmInitFailed) return null;
|
|
||||||
if (_wasmSolver) return _wasmSolver;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const solver = new DeepSeekHashWasm();
|
|
||||||
const wasmPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "sha3_wasm_bg.wasm");
|
|
||||||
await solver.init(wasmPath);
|
|
||||||
_wasmSolver = solver;
|
|
||||||
return solver;
|
|
||||||
} catch {
|
|
||||||
_wasmInitFailed = true;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── JS fallback solver (slow — ~5-6s at difficulty 144000) ───────────────
|
function createAbortError(): Error {
|
||||||
|
const error = new Error("DeepSeek PoW computation aborted");
|
||||||
const require = createRequire(import.meta.url);
|
error.name = "AbortError";
|
||||||
let _U: any | undefined;
|
return error;
|
||||||
function loadU(): any {
|
|
||||||
if (_U === undefined) {
|
|
||||||
_U = require("./deepseek-pow-solver.cjs").U;
|
|
||||||
}
|
|
||||||
return _U;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function solveWithJS(challenge: string, prefix: string, difficulty: number): number {
|
function validateChallenge(
|
||||||
const U = loadU();
|
algorithm: string,
|
||||||
const createHash = () => {
|
challenge: string,
|
||||||
const self: any = {};
|
salt: string,
|
||||||
self._sponge = new U({ capacity: 256, padding: 6 });
|
difficulty: number,
|
||||||
self.update = (s: string) => {
|
expireAt: number
|
||||||
self._sponge.absorb(Buffer.from(s, "utf8"));
|
): ValidatedChallenge {
|
||||||
return self;
|
if (algorithm !== DEEPSEEK_POW_ALGORITHM) {
|
||||||
};
|
throw new Error(`Unsupported DeepSeek PoW algorithm: ${algorithm}`);
|
||||||
self.digest = (fmt?: string) => {
|
}
|
||||||
return self._sponge.squeeze(6).toString(fmt || "hex");
|
if (!SHA3_256_HEX_PATTERN.test(challenge)) {
|
||||||
};
|
throw new Error("DeepSeek PoW challenge must be a 64-character SHA3-256 hex digest");
|
||||||
self.copy = () => {
|
}
|
||||||
const c: any = {};
|
if (typeof salt !== "string" || salt.length === 0 || salt.length > MAX_SALT_LENGTH) {
|
||||||
c._sponge = self._sponge.copy();
|
throw new Error(`DeepSeek PoW salt must contain 1-${MAX_SALT_LENGTH} characters`);
|
||||||
c.update = (s: string) => {
|
}
|
||||||
c._sponge.absorb(Buffer.from(s, "utf8"));
|
if (!Number.isSafeInteger(difficulty) || difficulty < 1 || difficulty > MAX_DIFFICULTY) {
|
||||||
return c;
|
throw new Error(`DeepSeek PoW difficulty must be an integer from 1 to ${MAX_DIFFICULTY}`);
|
||||||
};
|
}
|
||||||
c.digest = (fmt?: string) => {
|
if (!Number.isSafeInteger(expireAt) || expireAt < 0) {
|
||||||
return c._sponge.squeeze(6).toString(fmt || "hex");
|
throw new Error("DeepSeek PoW expiry must be a non-negative safe integer");
|
||||||
};
|
}
|
||||||
return c;
|
|
||||||
};
|
return {
|
||||||
return self;
|
challenge: challenge.toLowerCase(),
|
||||||
|
prefix: `${salt}_${expireAt}_`,
|
||||||
|
difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
const h = createHash();
|
|
||||||
h.update(prefix);
|
|
||||||
|
|
||||||
for (let nonce = 0; nonce < difficulty; nonce++) {
|
|
||||||
if (h.copy().update(String(nonce)).digest("hex") === challenge) {
|
|
||||||
return nonce;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API ───────────────────────────────────────────────────────────
|
function resolveTimeoutMs(timeoutMs: number | undefined): number {
|
||||||
|
if (timeoutMs === undefined) return DEFAULT_TIMEOUT_MS;
|
||||||
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
|
||||||
|
throw new Error(`DeepSeek PoW timeout must be an integer from 1 to ${MAX_TIMEOUT_MS}ms`);
|
||||||
|
}
|
||||||
|
return timeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function solveSynchronously({ challenge, prefix, difficulty }: ValidatedChallenge): number {
|
||||||
|
return findDeepSeekPowNonce(prefix, challenge, difficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWorkerPath(): string {
|
||||||
|
const tracedPath = path.join(process.cwd(), "open-sse/lib/deepseek-pow-worker.mjs");
|
||||||
|
if (existsSync(tracedPath)) return tracedPath;
|
||||||
|
return fileURLToPath(new URL("./deepseek-pow-worker.mjs", import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
function solveInWorker(
|
||||||
|
validated: ValidatedChallenge,
|
||||||
|
options: SolveDeepSeekPowOptions
|
||||||
|
): Promise<number> {
|
||||||
|
const { signal } = options;
|
||||||
|
if (signal?.aborted) return Promise.reject(createAbortError());
|
||||||
|
if (activeWorkerCount >= MAX_CONCURRENT_WORKERS) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(`DeepSeek PoW worker capacity reached (${MAX_CONCURRENT_WORKERS})`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutMs = resolveTimeoutMs(options.timeoutMs);
|
||||||
|
activeWorkerCount += 1;
|
||||||
|
|
||||||
|
return new Promise<number>((resolve, reject) => {
|
||||||
|
const worker = new Worker(resolveWorkerPath(), { workerData: validated });
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
signal?.removeEventListener("abort", onAbort);
|
||||||
|
activeWorkerCount = Math.max(0, activeWorkerCount - 1);
|
||||||
|
};
|
||||||
|
const finish = (callback: () => void, terminate: boolean) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
if (terminate) void worker.terminate();
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
const onAbort = () => {
|
||||||
|
finish(() => reject(createAbortError()), true);
|
||||||
|
};
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
finish(() => reject(new Error(`DeepSeek PoW computation exceeded ${timeoutMs}ms`)), true);
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
signal?.addEventListener("abort", onAbort, { once: true });
|
||||||
|
worker.once("message", (answer: unknown) => {
|
||||||
|
if (!Number.isSafeInteger(answer) || (answer as number) < -1) {
|
||||||
|
finish(() => reject(new Error("DeepSeek PoW worker returned an invalid answer")), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finish(() => resolve(answer as number), false);
|
||||||
|
});
|
||||||
|
worker.once("error", (error) => {
|
||||||
|
finish(() => reject(error), true);
|
||||||
|
});
|
||||||
|
worker.once("exit", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
finish(() => reject(new Error(`DeepSeek PoW worker exited with code ${code}`)), false);
|
||||||
|
} else {
|
||||||
|
finish(() => reject(new Error("DeepSeek PoW worker exited without an answer")), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function solveDeepSeekPowAsync(
|
export async function solveDeepSeekPowAsync(
|
||||||
algorithm: string,
|
algorithm: string,
|
||||||
challenge: string,
|
challenge: string,
|
||||||
salt: string,
|
salt: string,
|
||||||
difficulty: number,
|
difficulty: number,
|
||||||
expireAt: number
|
expireAt: number,
|
||||||
|
options: SolveDeepSeekPowOptions = {}
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`);
|
const validated = validateChallenge(algorithm, challenge, salt, difficulty, expireAt);
|
||||||
const prefix = `${salt}_${expireAt}_`;
|
return solveInWorker(validated, options);
|
||||||
|
|
||||||
const wasm = await getWasmSolver();
|
|
||||||
if (wasm) {
|
|
||||||
const answer = wasm.calculateHash(challenge, prefix, difficulty);
|
|
||||||
if (answer === undefined) return -1;
|
|
||||||
return answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return solveWithJS(challenge, prefix, difficulty);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync wrapper kept for backward compat (uses JS fallback only)
|
|
||||||
export function solveDeepSeekPow(
|
export function solveDeepSeekPow(
|
||||||
algorithm: string,
|
algorithm: string,
|
||||||
challenge: string,
|
challenge: string,
|
||||||
@@ -183,7 +158,9 @@ export function solveDeepSeekPow(
|
|||||||
difficulty: number,
|
difficulty: number,
|
||||||
expireAt: number
|
expireAt: number
|
||||||
): number {
|
): number {
|
||||||
if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`);
|
// Compatibility-only synchronous API. The validated 250k ceiling bounds CPU
|
||||||
const prefix = `${salt}_${expireAt}_`;
|
// use; request handling must use solveDeepSeekPowAsync() so hashing stays off
|
||||||
return solveWithJS(challenge, prefix, difficulty);
|
// the event loop and remains abortable.
|
||||||
|
const validated = validateChallenge(algorithm, challenge, salt, difficulty, expireAt);
|
||||||
|
return solveSynchronously(validated);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
238
tests/unit/deepseek-pow-js-only.test.ts
Normal file
238
tests/unit/deepseek-pow-js-only.test.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { solveDeepSeekPow, solveDeepSeekPowAsync } from "../../open-sse/lib/deepseek-pow.ts";
|
||||||
|
import {
|
||||||
|
deepSeekHashV1,
|
||||||
|
deepSeekHashV1Reference,
|
||||||
|
sha3_256Fips202Reference,
|
||||||
|
} from "../../open-sse/lib/deepseek-pow-hash.js";
|
||||||
|
|
||||||
|
const repoRoot = new URL("../../", import.meta.url);
|
||||||
|
const noMatchChallenge = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
|
||||||
|
const redistributedArtifacts = [
|
||||||
|
new URL("../../open-sse/lib/sha3_wasm_bg.wasm", import.meta.url),
|
||||||
|
new URL("../../open-sse/lib/deepseek-pow-solver.cjs", import.meta.url),
|
||||||
|
];
|
||||||
|
|
||||||
|
const deepSeekHashV1Vectors = [
|
||||||
|
[
|
||||||
|
"1122334455667788_1778891543095_0",
|
||||||
|
"311b26ae1e0fe7375e242958ce46db5552a6c67fea3f96880dcd846c63a74286",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1122334455667788_1778891543095_1",
|
||||||
|
"526f9103dfe22bcda9481b7f304a157b8edb18c5bb96a2061eefec1fbd0db706",
|
||||||
|
],
|
||||||
|
["vector_42_0", "1813da791ddac0da3a225e5878ff3d3ea07577e048f8e9575b82a274b71e3810"],
|
||||||
|
["vector_42_1", "b582fdb4f83662baec734e23373efc860515c03c40decbef076697eb5832f83c"],
|
||||||
|
["vector_42_2", "48b348ad54c372f78ccb6a26cbf156668f4c6a5b7d5a1cf19ccd78888586019b"],
|
||||||
|
["vector_42_3", "2ffed26ea9e1d6f4bbe49a266d98fe04ad9a5ad4c6d765862de8d18c513c3815"],
|
||||||
|
["bb_1778891543095_0", "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
test("DeepSeekHashV1 is SHA3-256 with the last 23 KECCAK-p[1600] rounds", () => {
|
||||||
|
for (const [input, expected] of deepSeekHashV1Vectors) {
|
||||||
|
assert.equal(deepSeekHashV1Reference(input), expected, input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the 24-round FIPS 202 reference agrees with node:crypto on a separate corpus", () => {
|
||||||
|
const corpus = [
|
||||||
|
"",
|
||||||
|
"abc",
|
||||||
|
"café",
|
||||||
|
"🧪",
|
||||||
|
"a".repeat(135),
|
||||||
|
"b".repeat(136),
|
||||||
|
"c".repeat(137),
|
||||||
|
"multi-block-".repeat(40),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const input of corpus) {
|
||||||
|
const expected = createHash("sha3-256").update(input).digest("hex");
|
||||||
|
assert.equal(sha3_256Fips202Reference(input), expected, JSON.stringify(input));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the optimized DeepSeekHashV1 implementation agrees with the readable reference", () => {
|
||||||
|
const corpus = [
|
||||||
|
...deepSeekHashV1Vectors.map(([input]) => input),
|
||||||
|
"",
|
||||||
|
"café-🧪",
|
||||||
|
"a".repeat(135),
|
||||||
|
"b".repeat(136),
|
||||||
|
"c".repeat(137),
|
||||||
|
"multi-block-".repeat(40),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const input of corpus) {
|
||||||
|
assert.equal(deepSeekHashV1(input), deepSeekHashV1Reference(input), JSON.stringify(input));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DeepSeek PoW remains functional without redistributing extracted solver artifacts", async () => {
|
||||||
|
const answer = await solveDeepSeekPowAsync(
|
||||||
|
"DeepSeekHashV1",
|
||||||
|
"311b26ae1e0fe7375e242958ce46db5552a6c67fea3f96880dcd846c63a74286",
|
||||||
|
"1122334455667788",
|
||||||
|
1,
|
||||||
|
1778891543095
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(answer, 0, "the retained JavaScript solver must satisfy the known PoW vector");
|
||||||
|
for (const artifact of redistributedArtifacts) {
|
||||||
|
assert.equal(existsSync(artifact), false, `${artifact.pathname} must not be redistributed`);
|
||||||
|
}
|
||||||
|
assert.equal(
|
||||||
|
existsSync(new URL("../../open-sse/lib/deepseek-pow-hash.js", import.meta.url)),
|
||||||
|
true,
|
||||||
|
"the clean-room JavaScript hash core must be packaged"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
existsSync(new URL("../../open-sse/lib/deepseek-pow-worker.mjs", import.meta.url)),
|
||||||
|
true,
|
||||||
|
"the asynchronous worker entry must be packaged"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const relativePath of [
|
||||||
|
"open-sse/lib/deepseek-pow-hash.js",
|
||||||
|
"open-sse/lib/deepseek-pow.ts",
|
||||||
|
"open-sse/lib/deepseek-pow-worker.mjs",
|
||||||
|
"next.config.mjs",
|
||||||
|
"package.json",
|
||||||
|
"open-sse/package.json",
|
||||||
|
]) {
|
||||||
|
const contents = readFileSync(new URL(relativePath, repoRoot), "utf8");
|
||||||
|
assert.doesNotMatch(
|
||||||
|
contents,
|
||||||
|
/sha3_wasm_bg\.wasm|deepseek-pow-solver\.cjs/,
|
||||||
|
`${relativePath} must not reference an extracted solver artifact`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const relativePath of [
|
||||||
|
"open-sse/lib/deepseek-pow-hash.js",
|
||||||
|
"open-sse/lib/deepseek-pow.ts",
|
||||||
|
"open-sse/lib/deepseek-pow-worker.mjs",
|
||||||
|
]) {
|
||||||
|
const contents = readFileSync(new URL(relativePath, repoRoot), "utf8");
|
||||||
|
assert.doesNotMatch(
|
||||||
|
contents,
|
||||||
|
/createRequire|WebAssembly/,
|
||||||
|
`${relativePath} must not dynamically load a binary solver`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextConfig = readFileSync(new URL("../../next.config.mjs", import.meta.url), "utf8");
|
||||||
|
assert.match(nextConfig, /\.\/open-sse\/lib\/deepseek-pow-hash\.js/);
|
||||||
|
assert.match(nextConfig, /\.\/open-sse\/lib\/deepseek-pow-worker\.mjs/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DeepSeek PoW rejects malformed or unsafe challenges before hashing", async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
solveDeepSeekPowAsync("DeepSeekHashV1", "not-a-sha3-digest", "salt", 1, 1),
|
||||||
|
/challenge/i
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
solveDeepSeekPowAsync(
|
||||||
|
"DeepSeekHashV1",
|
||||||
|
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
"salt",
|
||||||
|
-1,
|
||||||
|
1
|
||||||
|
),
|
||||||
|
/difficulty/i
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
solveDeepSeekPowAsync("DeepSeekHashV1", noMatchChallenge, "salt", 250_001, 1),
|
||||||
|
/difficulty/i
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => solveDeepSeekPow("DeepSeekHashV1", noMatchChallenge, "salt", 250_001, 1),
|
||||||
|
/difficulty/i
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
solveDeepSeekPow(
|
||||||
|
"UnknownHash",
|
||||||
|
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
"salt",
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
),
|
||||||
|
/unsupported/i
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DeepSeek PoW sync and async APIs agree for non-zero and missing answers", async () => {
|
||||||
|
const nonceThreeChallenge = "2ffed26ea9e1d6f4bbe49a266d98fe04ad9a5ad4c6d765862de8d18c513c3815";
|
||||||
|
|
||||||
|
assert.equal(solveDeepSeekPow("DeepSeekHashV1", nonceThreeChallenge, "vector", 4, 42), 3);
|
||||||
|
assert.equal(
|
||||||
|
await solveDeepSeekPowAsync("DeepSeekHashV1", nonceThreeChallenge, "vector", 4, 42),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
assert.equal(solveDeepSeekPow("DeepSeekHashV1", nonceThreeChallenge, "vector", 3, 42), -1);
|
||||||
|
assert.equal(
|
||||||
|
await solveDeepSeekPowAsync("DeepSeekHashV1", nonceThreeChallenge, "vector", 3, 42),
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DeepSeek PoW async search yields to timers and honors cancellation", async () => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
let timerFired = false;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
timerFired = true;
|
||||||
|
controller.abort();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
solveDeepSeekPowAsync(
|
||||||
|
"DeepSeekHashV1",
|
||||||
|
noMatchChallenge,
|
||||||
|
"1122334455667788",
|
||||||
|
256,
|
||||||
|
1778891543095,
|
||||||
|
{ signal: controller.signal }
|
||||||
|
),
|
||||||
|
(error: unknown) => error instanceof Error && error.name === "AbortError"
|
||||||
|
);
|
||||||
|
assert.equal(timerFired, true, "the event loop must progress while PoW is being searched");
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DeepSeek PoW async search enforces a deterministic timeout", async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
solveDeepSeekPowAsync("DeepSeekHashV1", noMatchChallenge, "timeout", 250_000, 1, {
|
||||||
|
timeoutMs: 1,
|
||||||
|
}),
|
||||||
|
/exceeded 1ms/i
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DeepSeek PoW caps concurrent worker searches at two", async () => {
|
||||||
|
const controllers = [new AbortController(), new AbortController()];
|
||||||
|
const searches = controllers.map((controller) =>
|
||||||
|
solveDeepSeekPowAsync("DeepSeekHashV1", noMatchChallenge, "capacity", 250_000, 1, {
|
||||||
|
signal: controller.signal,
|
||||||
|
timeoutMs: 30_000,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
solveDeepSeekPowAsync("DeepSeekHashV1", noMatchChallenge, "capacity", 1, 1),
|
||||||
|
/capacity reached \(2\)/i
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
for (const controller of controllers) controller.abort();
|
||||||
|
await Promise.allSettled(searches);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import test from "node:test";
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { createRequire } from "node:module";
|
|
||||||
|
|
||||||
const requireCJS = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
test("deepseek-pow-solver.cjs loads under Node strict mode without ReferenceError (#2724)", () => {
|
|
||||||
let mod: { U?: unknown };
|
|
||||||
assert.doesNotThrow(() => {
|
|
||||||
mod = requireCJS("../../open-sse/lib/deepseek-pow-solver.cjs");
|
|
||||||
});
|
|
||||||
assert.ok(mod!, "module loaded");
|
|
||||||
assert.equal(typeof mod!.U, "function", "expected exported constructor U");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("deepseek-pow-solver.cjs does not pollute globalThis.onmessage in Node (#2724)", () => {
|
|
||||||
const before = (globalThis as { onmessage?: unknown }).onmessage;
|
|
||||||
requireCJS("../../open-sse/lib/deepseek-pow-solver.cjs");
|
|
||||||
const after = (globalThis as { onmessage?: unknown }).onmessage;
|
|
||||||
assert.equal(after, before, "onmessage must remain unchanged outside Worker context");
|
|
||||||
});
|
|
||||||
@@ -648,24 +648,73 @@ test("execute with a new DeepSeek userToken restarts auto-refresh", async () =>
|
|||||||
|
|
||||||
// ─── Abort handling ──────────────────────────────────────────────────────
|
// ─── Abort handling ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
test("execute: handles abort signal gracefully", async () => {
|
test("execute: propagates the request signal into an in-flight PoW search", async () => {
|
||||||
const dsMod4 = await import("../../open-sse/executors/deepseek-web.ts");
|
const dsMod4 = await import("../../open-sse/executors/deepseek-web.ts");
|
||||||
if (dsMod4.tokenCache) dsMod4.tokenCache.clear();
|
dsMod4.tokenCache.clear();
|
||||||
const executor = new DeepSeekWebExecutor();
|
dsMod4.sessionCache.clear();
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
controller.abort();
|
let completionCalls = 0;
|
||||||
const result = await executor.execute({
|
|
||||||
model: "default",
|
globalThis.fetch = async (input) => {
|
||||||
body: { messages: [{ role: "user", content: "hi" }] },
|
const url = input.toString();
|
||||||
stream: true,
|
if (url.includes("/users/current")) {
|
||||||
credentials: { apiKey: "test-token-abort" },
|
return new Response(
|
||||||
signal: controller.signal,
|
JSON.stringify({ code: 0, data: { biz_data: { token: "abort-access-token" } } }),
|
||||||
});
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||||
assert.ok(result.response, "Should return response");
|
);
|
||||||
assert.ok(
|
}
|
||||||
result.response.status >= 400 || result.response.status === 499,
|
if (url.includes("/chat_session/create")) {
|
||||||
"Should indicate error or abort"
|
return new Response(
|
||||||
);
|
JSON.stringify({
|
||||||
|
code: 0,
|
||||||
|
data: { biz_data: { chat_session: { id: "abort-session" } } },
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (url.includes("/chat/create_pow_challenge")) {
|
||||||
|
setTimeout(() => controller.abort(), 0);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
biz_data: {
|
||||||
|
challenge: {
|
||||||
|
algorithm: "DeepSeekHashV1",
|
||||||
|
challenge: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
salt: "abort",
|
||||||
|
signature: "test-signature",
|
||||||
|
difficulty: 50_000,
|
||||||
|
expire_at: 1,
|
||||||
|
expire_after: 1,
|
||||||
|
target_path: "/api/v0/chat/completion",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (url.includes("/chat/completion")) completionCalls += 1;
|
||||||
|
throw new Error(`Unexpected request after PoW cancellation: ${url}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await new DeepSeekWebExecutor().execute({
|
||||||
|
model: "default",
|
||||||
|
body: { messages: [{ role: "user", content: "hi" }] },
|
||||||
|
stream: false,
|
||||||
|
credentials: { apiKey: "test-token-abort" },
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
assert.equal(result.response.status, 499);
|
||||||
|
assert.equal(completionCalls, 0, "an aborted request must not reach completion");
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
dsMod4.tokenCache.clear();
|
||||||
|
dsMod4.sessionCache.clear();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Search enabled ──────────────────────────────────────────────────────
|
// ─── Search enabled ──────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user