fix: do scoreID hashing better

This commit is contained in:
zkldi
2023-01-19 16:39:53 +00:00
parent 6fbea2ca97
commit 90c4d302c7
3 changed files with 12 additions and 27 deletions
+2
View File
@@ -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) => {
@@ -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.**"
);
@@ -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<string, number | string> = { 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}`;
}