Football / src /services /matchProof.js
Prataykarali's picture
feat: deploy sustainability, staff command center, security hardening, accessibility and UX polish to HF
ca26788
Raw
History Blame Contribute Delete
7.85 kB
/**
* Match Proof Layer — Cairo-ready session proof generator
*/
export class MatchProofService {
constructor() {}
canonicalize(input) {
if (typeof input === 'string') return input;
if (typeof input !== 'object' || input === null) return String(input);
const sortObjectKeys = (obj) => {
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
if (typeof obj === 'object' && obj !== null) {
return Object.keys(obj)
.sort()
.reduce((acc, key) => {
acc[key] = sortObjectKeys(obj[key]);
return acc;
}, {});
}
return obj;
};
return JSON.stringify(sortObjectKeys(input));
}
buildCanonicalPayload(sessionData) {
return this.canonicalize(sessionData);
}
async generateProofId(input) {
const canonical = this.canonicalize(input);
return this._sha256(canonical);
}
async generateEngagementFingerprint(categoryTotals = {}) {
const canonical = this.canonicalize(categoryTotals);
return this._sha256(canonical);
}
toFelt252Sync(hexHash) {
if (!hexHash) return BigInt(0);
const cleanHex = hexHash.replace(/^0x/, '');
const first31Bytes = cleanHex.substring(0, 62);
const val = BigInt('0x' + (first31Bytes || '0'));
const maxFelt = (BigInt(2) ** BigInt(248)) - BigInt(1);
return val % maxFelt;
}
/** Async version for backward-compat — delegates to sync implementation. */
async toFelt252(input) {
let hexHash;
if (typeof input === 'string') {
hexHash = input;
} else {
hexHash = await this.generateProofId(input);
}
return this.toFelt252Sync(hexHash);
}
async toCairoCalldata(input) {
const proofIdHex = await this.generateProofId(input);
const proofIdFelt = await this.toFelt252(proofIdHex);
const engagement = input.engagement || input.categoryTotals || {};
const fingerprintHex = await this.generateEngagementFingerprint(engagement);
const fingerprintFelt = await this.toFelt252(fingerprintHex);
const ts = input.timestamp || Math.floor(Date.now() / 1000);
return {
proof_id: proofIdFelt.toString(),
proof_id_hex: proofIdHex,
accuracy_bps: input.accuracyBps || 0,
engagement_fingerprint: fingerprintFelt.toString(),
engagement_fingerprint_hex: fingerprintHex,
recorded_at: ts,
timestamp: ts
};
}
buildCairoCalldata(proofIdHex, accuracyBps, fingerprintHex, timestamp) {
const proofIdFelt = this.toFelt252Sync(proofIdHex);
const fingerprintFelt = this.toFelt252Sync(fingerprintHex);
return {
proof_id: proofIdFelt.toString(),
proof_id_hex: proofIdHex,
accuracy_bps: accuracyBps,
engagement_fingerprint: fingerprintFelt.toString(),
engagement_fingerprint_hex: fingerprintHex,
recorded_at: timestamp,
timestamp: timestamp
};
}
async exportProof(calldataOrInput) {
let calldata;
if (calldataOrInput && calldataOrInput.proof_id) {
calldata = calldataOrInput;
} else {
calldata = await this.toCairoCalldata(calldataOrInput);
}
const jsonStr = JSON.stringify(calldata, null, 2);
if (typeof document !== 'undefined') {
try {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `vantage_proof_${calldata.recorded_at || Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch {
// Fallback for non-DOM environments
}
}
return jsonStr;
}
async verifyProof(sessionData, expectedProofIdHex) {
const actualProofIdHex = await this.generateProofId(sessionData);
return actualProofIdHex === expectedProofIdHex;
}
async _sha256(str) {
if (typeof crypto !== 'undefined' && crypto.subtle) {
try {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
} catch {
// fallback
}
}
// Secure, standard pure JS SHA-256 fallback
const rightRotate = (value, amount) => (value >>> amount) | (value << (32 - amount));
const mathPow = Math.pow;
const maxWord = mathPow(2, 32);
const result = [];
const words = [];
const asciiLength = str.length;
const hash = [];
const k = [];
let primeCounter = 0;
const isPrime = (n) => {
for (let factor = 2; factor * factor <= n; factor++) {
if (n % factor === 0) return false;
}
return true;
};
let candidate = 2;
while (primeCounter < 64) {
if (isPrime(candidate)) {
if (primeCounter < 8) {
hash[primeCounter] = (mathPow(candidate, 1/2) * maxWord) | 0;
}
k[primeCounter] = (mathPow(candidate, 1/3) * maxWord) | 0;
primeCounter++;
}
candidate++;
}
const utf8 = [];
for (let i = 0; i < asciiLength; i++) {
let charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
} else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f));
} else {
i++;
charcode = 0x10000 + (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
utf8.push(
0xf0 | (charcode >> 18),
0x80 | ((charcode >> 12) & 0x3f),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f)
);
}
}
const utf8Length = utf8.length;
utf8.push(0x80);
while (utf8.length % 64 !== 56) utf8.push(0);
const totalBits = utf8Length * 8;
for (let i = 7; i >= 0; i--) {
utf8.push((totalBits >>> (i * 8)) & 0xff);
}
for (let i = 0; i < utf8.length; i += 4) {
words.push((utf8[i] << 24) | (utf8[i+1] << 16) | (utf8[i+2] << 8) | utf8[i+3]);
}
for (let i = 0; i < words.length; i += 16) {
const w = new Array(64);
for (let j = 0; j < 16; j++) w[j] = words[i + j];
for (let j = 16; j < 64; j++) {
const s0 = rightRotate(w[j - 15], 7) ^ rightRotate(w[j - 15], 18) ^ (w[j - 15] >>> 3);
const s1 = rightRotate(w[j - 2], 17) ^ rightRotate(w[j - 2], 19) ^ (w[j - 2] >>> 10);
w[j] = (w[j - 16] + s0 + w[j - 7] + s1) | 0;
}
let a = hash[0], b = hash[1], c = hash[2], d = hash[3], e = hash[4], f = hash[5], g = hash[6], h = hash[7];
for (let j = 0; j < 64; j++) {
const S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
const ch = (e & f) ^ (~e & g);
const temp1 = (h + S1 + ch + k[j] + w[j]) | 0;
const S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
const maj = (a & b) ^ (a & c) ^ (b & c);
const temp2 = (S0 + maj) | 0;
h = g;
g = f;
f = e;
e = (d + temp1) | 0;
d = c;
c = b;
b = a;
a = (temp1 + temp2) | 0;
}
hash[0] = (hash[0] + a) | 0;
hash[1] = (hash[1] + b) | 0;
hash[2] = (hash[2] + c) | 0;
hash[3] = (hash[3] + d) | 0;
hash[4] = (hash[4] + e) | 0;
hash[5] = (hash[5] + f) | 0;
hash[6] = (hash[6] + g) | 0;
hash[7] = (hash[7] + h) | 0;
}
for (let i = 0; i < 8; i++) {
const hexStr = (hash[i] >>> 0).toString(16).padStart(8, '0');
result.push(hexStr);
}
return result.join('');
}
}