diff --git a/common/src/config/config.test.ts b/common/src/config/config.test.ts index bd10cc2a9..ec4fb47a3 100644 --- a/common/src/config/config.test.ts +++ b/common/src/config/config.test.ts @@ -23,6 +23,8 @@ const BANNED_METRIC_NAMES = [ "enumIndexes", // haha "optional", // used for optional metrics "playcount", // used by showcase stats + "scoreID", + "userID", ]; t.test("#GetGamePTConfig", (t) => { diff --git a/server/src/lib/score-import/framework/score-importing/score-id.test.ts b/server/src/lib/score-import/framework/score-importing/score-id.test.ts index 31c96a5b9..d2be0dd56 100644 --- a/server/src/lib/score-import/framework/score-importing/score-id.test.ts +++ b/server/src/lib/score-import/framework/score-importing/score-id.test.ts @@ -167,7 +167,7 @@ t.test("#ScoreID Canary", (t) => { t.equal( scoreID, - "Tc4f10c2ca8e99478de5ae318a40ee66051f7297ca648415502b6f393a23e6f10", + "T5d669c4d5d6ca80761e87698acd77c51d2bed95b64ab76e65952dbca7c26bc81", "ScoreID **SHOULD BE DETERMINISTIC**. A change to the ScoreID algorithm is **a major breaking change.**" ); diff --git a/server/src/lib/score-import/framework/score-importing/score-id.ts b/server/src/lib/score-import/framework/score-importing/score-id.ts index 55e89a85a..66230d7a2 100644 --- a/server/src/lib/score-import/framework/score-importing/score-id.ts +++ b/server/src/lib/score-import/framework/score-importing/score-id.ts @@ -1,22 +1,9 @@ import db from "external/mongo/db"; +import fjsh from "fast-json-stable-hash"; import { GetGPTConfig } from "tachi-common"; -import crypto from "crypto"; import type { DryScore } from "../common/types"; import type { integer, GPTString, ProvidedMetrics, OptionalMetrics } from "tachi-common"; -/** - * Performs sha256 hashing on the input data. - * @param scoreIDString - The string to sha256 hash. - * @returns A sha256 checksum in lowercase hex. - */ -function HashScoreIDString(scoreIDString: string) { - return crypto.createHash("sha256").update(scoreIDString).digest("hex"); -} - -function SortKeysAlphabetically(a: string, b: string) { - return a.localeCompare(b, "en-GB"); -} - /** * Creates an identifier for this score. * This is used to deduplicate repeated scores. @@ -28,34 +15,30 @@ export function CreateScoreID( dryScore: DryScore, chartID: string ) { - const elements = [userID, chartID]; + const elements: Record = { userID, chartID }; const gptConfig = GetGPTConfig(gptString); - // @warn - // we need to sort these metric keys deterministically instead - // of relying on any sort of object-insertion order - // as that would throw the checksum out of sync. - for (const m of Object.keys(gptConfig.providedMetrics).sort(SortKeysAlphabetically)) { + for (const m of Object.keys(gptConfig.providedMetrics)) { const metric = m as keyof ProvidedMetrics[GPTString]; - elements.push(dryScore.scoreData[metric]); + elements[metric] = dryScore.scoreData[metric]; } // Also include optional metrics in the checksum if they should be // part of the scoreID. - for (const [m, conf] of Object.entries(gptConfig.optionalMetrics).sort((a, b) => - SortKeysAlphabetically(a[0], b[0]) - )) { + for (const [m, conf] of Object.entries(gptConfig.optionalMetrics)) { const metric = m as keyof OptionalMetrics[GPTString]; if (conf.partOfScoreID) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - elements.push(dryScore.scoreData.optional[metric] ?? null); + elements[`optional.${metric}`] = dryScore.scoreData.optional[metric] ?? null; } } - const hash = HashScoreIDString(elements.join("\0")); + // use a stable object hashing method instead of string joining + // as it's immune to key order or anything screwy like that. + const hash = fjsh.hash(elements, "sha256"); return `T${hash}`; }