feat(core): Phase 1.3 Public Verification Integrity - drawId persistence, real participant snapshot and rules recalculation, reserve winners check, auditEventHash verification, strict snapshot lookup, and anti-tampering test suite
This commit is contained in:
parent
1bc6650041
commit
26e82fcf8d
11 changed files with 511 additions and 97 deletions
|
|
@ -89,6 +89,7 @@ model ParticipantSnapshot {
|
||||||
version Int @default(1)
|
version Int @default(1)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
eligibleParticipants Json // Canonical JSON array of FilteredParticipant
|
eligibleParticipants Json // Canonical JSON array of FilteredParticipant
|
||||||
|
filterRulesSnapshot Json // Canonical JSON snapshot of FilterRules
|
||||||
participantCount Int
|
participantCount Int
|
||||||
participantsSnapshotHash String
|
participantsSnapshotHash String
|
||||||
conditionsHash String
|
conditionsHash String
|
||||||
|
|
@ -102,6 +103,7 @@ model ParticipantSnapshot {
|
||||||
|
|
||||||
model DrawResult {
|
model DrawResult {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
drawId String // Original domain drawId (e.g. draw_8f9102ab...)
|
||||||
giveawayId String @unique
|
giveawayId String @unique
|
||||||
giveaway Giveaway @relation(fields: [giveawayId], references: [id], onDelete: Cascade)
|
giveaway Giveaway @relation(fields: [giveawayId], references: [id], onDelete: Cascade)
|
||||||
snapshotId String @unique
|
snapshotId String @unique
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,14 @@ export async function GET(
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the snapshot associated with this draw
|
// Strict snapshot lookup: DO NOT fallback to latestSnapshot
|
||||||
const snapshot = giveaway.snapshots.find(s => s.id === drawResult.snapshotId)
|
const snapshot = giveaway.snapshots.find(s => s.id === drawResult.snapshotId);
|
||||||
|| giveaway.latestSnapshot;
|
|
||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
error: `Participant snapshot "${drawResult.snapshotId}" not found for this giveaway`
|
error: `Integrity Error: Participant snapshot "${drawResult.snapshotId}" referenced by draw does not exist in storage`,
|
||||||
|
verified: false,
|
||||||
|
snapshotFound: false,
|
||||||
}, { status: 404 });
|
}, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,29 +36,38 @@ export async function GET(
|
||||||
const claimedReserveCount = drawResult.reserveWinners.length;
|
const claimedReserveCount = drawResult.reserveWinners.length;
|
||||||
|
|
||||||
// Run independent cryptographic replay verification
|
// Run independent cryptographic replay verification
|
||||||
const verification = verifyDrawResult(
|
const verification = verifyDrawResult({
|
||||||
|
giveawayId: id,
|
||||||
|
drawId: drawResult.drawId,
|
||||||
|
drawnAt: drawResult.drawnAt,
|
||||||
snapshot,
|
snapshot,
|
||||||
drawResult.seedUsed,
|
seed: drawResult.seedUsed,
|
||||||
claimedWinnersCount,
|
claimedWinnersCount,
|
||||||
claimedReserveCount,
|
claimedReserveCount,
|
||||||
drawResult.winnerIds,
|
claimedWinnerIds: drawResult.winnerIds,
|
||||||
drawResult.deterministicProofHash,
|
claimedReserveWinnerIds: drawResult.reserveWinnerIds,
|
||||||
drawResult.algorithmVersion
|
claimedDeterministicProofHash: drawResult.deterministicProofHash,
|
||||||
);
|
claimedAuditEventHash: drawResult.auditEventHash,
|
||||||
|
algorithmVersion: drawResult.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
verified: verification.verified,
|
verified: verification.verified,
|
||||||
giveawayId: id,
|
giveawayId: id,
|
||||||
|
drawId: drawResult.drawId,
|
||||||
snapshotId: snapshot.id,
|
snapshotId: snapshot.id,
|
||||||
algorithmVersion: verification.algorithmVersion,
|
algorithmVersion: verification.algorithmVersion,
|
||||||
|
algorithmSupported: verification.algorithmSupported,
|
||||||
|
participantsSnapshotIntegrity: verification.participantsSnapshotIntegrity,
|
||||||
|
conditionsIntegrity: verification.conditionsIntegrity,
|
||||||
winnersMatch: verification.winnersMatch,
|
winnersMatch: verification.winnersMatch,
|
||||||
snapshotHashMatch: verification.snapshotHashMatch,
|
reserveWinnersMatch: verification.reserveWinnersMatch,
|
||||||
conditionsHashMatch: verification.conditionsHashMatch,
|
|
||||||
deterministicProofHashMatch: verification.deterministicProofHashMatch,
|
deterministicProofHashMatch: verification.deterministicProofHashMatch,
|
||||||
|
auditEventHashMatch: verification.auditEventHashMatch,
|
||||||
expectedWinnerIds: verification.expectedWinnerIds,
|
expectedWinnerIds: verification.expectedWinnerIds,
|
||||||
expectedReserveWinnerIds: verification.expectedReserveWinnerIds,
|
expectedReserveWinnerIds: verification.expectedReserveWinnerIds,
|
||||||
deterministicProofHash: verification.expectedDeterministicProofHash,
|
deterministicProofHash: verification.expectedDeterministicProofHash,
|
||||||
auditEventHash: drawResult.auditEventHash,
|
auditEventHash: verification.expectedAuditEventHash,
|
||||||
drawnAt: drawResult.drawnAt,
|
drawnAt: drawResult.drawnAt,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ export default function GiveawayDetailPage() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(JSON.stringify({
|
navigator.clipboard.writeText(JSON.stringify({
|
||||||
giveawayId: giveaway.id,
|
giveawayId: giveaway.id,
|
||||||
|
drawId: drawResult.drawId,
|
||||||
snapshotId: drawResult.snapshotId,
|
snapshotId: drawResult.snapshotId,
|
||||||
algorithmVersion: drawResult.algorithmVersion,
|
algorithmVersion: drawResult.algorithmVersion,
|
||||||
seed: drawResult.seedUsed,
|
seed: drawResult.seedUsed,
|
||||||
|
|
@ -243,16 +244,22 @@ export default function GiveawayDetailPage() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 pt-1 font-mono text-[11px]">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 pt-1 font-mono text-[11px]">
|
||||||
|
<div>Целостность участников: {verificationResult.participantsSnapshotIntegrity ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
||||||
|
<div>Целостность условий: {verificationResult.conditionsIntegrity ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
||||||
<div>Победители совпали: {verificationResult.winnersMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
<div>Победители совпали: {verificationResult.winnersMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
||||||
<div>Хеш слепка совпал: {verificationResult.snapshotHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
<div>Резерв совпал: {verificationResult.reserveWinnersMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
||||||
<div>Хеш условий совпал: {verificationResult.conditionsHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
|
||||||
<div>Proof Hash совпал: {verificationResult.deterministicProofHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
<div>Proof Hash совпал: {verificationResult.deterministicProofHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
||||||
|
<div>Event Hash совпал: {verificationResult.auditEventHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
|
||||||
|
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800">
|
||||||
|
<span className="text-slate-400">Draw ID:</span>
|
||||||
|
<p className="font-mono text-amber-300 break-all">{drawResult.drawId}</p>
|
||||||
|
</div>
|
||||||
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800">
|
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800">
|
||||||
<span className="text-slate-400">Snapshot ID:</span>
|
<span className="text-slate-400">Snapshot ID:</span>
|
||||||
<p className="font-mono text-slate-300 break-all">{drawResult.snapshotId}</p>
|
<p className="font-mono text-slate-300 break-all">{drawResult.snapshotId}</p>
|
||||||
|
|
@ -269,6 +276,10 @@ export default function GiveawayDetailPage() {
|
||||||
<span className="text-slate-400">Snapshot Hash:</span>
|
<span className="text-slate-400">Snapshot Hash:</span>
|
||||||
<p className="font-mono text-emerald-400 break-all">{drawResult.participantsSnapshotHash}</p>
|
<p className="font-mono text-emerald-400 break-all">{drawResult.participantsSnapshotHash}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800">
|
||||||
|
<span className="text-slate-400">Conditions Hash:</span>
|
||||||
|
<p className="font-mono text-purple-400 break-all">{drawResult.conditionsHash}</p>
|
||||||
|
</div>
|
||||||
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800 sm:col-span-2">
|
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800 sm:col-span-2">
|
||||||
<span className="text-slate-400">deterministicProofHash (воспроизводимый):</span>
|
<span className="text-slate-400">deterministicProofHash (воспроизводимый):</span>
|
||||||
<p className="font-mono text-indigo-300 break-all">{drawResult.deterministicProofHash}</p>
|
<p className="font-mono text-indigo-300 break-all">{drawResult.deterministicProofHash}</p>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,16 @@ import {
|
||||||
DrawExecutionResult,
|
DrawExecutionResult,
|
||||||
ALGORITHM_HMAC_SHA256_FY_V1,
|
ALGORITHM_HMAC_SHA256_FY_V1,
|
||||||
ParticipantSnapshotData,
|
ParticipantSnapshotData,
|
||||||
|
VerificationParams,
|
||||||
VerificationResult
|
VerificationResult
|
||||||
} from '../types/audit';
|
} from '../types/audit';
|
||||||
import { DeterministicHmacStream } from './unbiased-sampler';
|
import { DeterministicHmacStream } from './unbiased-sampler';
|
||||||
import { computeDeterministicProofHash, computeAuditEventHash } from './canonical';
|
import {
|
||||||
|
computeDeterministicProofHash,
|
||||||
|
computeAuditEventHash,
|
||||||
|
computeParticipantsSnapshotHash,
|
||||||
|
computeConditionsHash
|
||||||
|
} from './canonical';
|
||||||
|
|
||||||
export const ALGORITHM_VERSION_V1 = ALGORITHM_HMAC_SHA256_FY_V1; // 'HMAC_SHA256_FY_V1'
|
export const ALGORITHM_VERSION_V1 = ALGORITHM_HMAC_SHA256_FY_V1; // 'HMAC_SHA256_FY_V1'
|
||||||
|
|
||||||
|
|
@ -161,55 +167,102 @@ export function executeDeterministicDraw(params: DrawExecutionParams): DrawExecu
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-runs draw algorithm on a snapshot to verify identical outcome and hashes
|
* Re-runs draw algorithm on a snapshot to verify identical outcome and all cryptographic integrity hashes
|
||||||
*/
|
*/
|
||||||
export function verifyDrawResult(
|
export function verifyDrawResult(params: VerificationParams): VerificationResult {
|
||||||
snapshot: ParticipantSnapshotData,
|
const {
|
||||||
seed: string,
|
giveawayId,
|
||||||
claimedWinnersCount: number,
|
drawId,
|
||||||
claimedReserveCount: number,
|
drawnAt,
|
||||||
claimedWinnerIds?: string[],
|
snapshot,
|
||||||
claimedDeterministicProofHash?: string,
|
seed,
|
||||||
algorithmVersion: string = ALGORITHM_VERSION_V1
|
claimedWinnersCount,
|
||||||
): VerificationResult {
|
claimedReserveCount,
|
||||||
if (algorithmVersion !== ALGORITHM_VERSION_V1) {
|
claimedWinnerIds = [],
|
||||||
throw new Error(`Unsupported algorithm version for replay: ${algorithmVersion}`);
|
claimedReserveWinnerIds = [],
|
||||||
|
claimedDeterministicProofHash = '',
|
||||||
|
claimedAuditEventHash = '',
|
||||||
|
algorithmVersion = ALGORITHM_VERSION_V1,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const algorithmSupported = (algorithmVersion === ALGORITHM_VERSION_V1);
|
||||||
|
|
||||||
|
// 1. Check Participant Snapshot Integrity: real recalculation from array
|
||||||
|
const computedSnapshotHash = computeParticipantsSnapshotHash(snapshot.eligibleParticipants || []);
|
||||||
|
const participantsSnapshotIntegrity = (computedSnapshotHash === snapshot.participantsSnapshotHash);
|
||||||
|
|
||||||
|
// 2. Check Conditions Integrity: real recalculation from filter rules snapshot
|
||||||
|
const computedConditionsHash = computeConditionsHash(snapshot.filterRulesSnapshot || {} as any);
|
||||||
|
const conditionsIntegrity = (computedConditionsHash === snapshot.conditionsHash);
|
||||||
|
|
||||||
|
// 3. Replay randomizer
|
||||||
|
let replayed: DrawExecutionResult | null = null;
|
||||||
|
let replayError = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
replayed = executeDeterministicDrawV1({
|
||||||
|
giveawayId,
|
||||||
|
snapshot,
|
||||||
|
totalLoadedCount: snapshot.participantCount,
|
||||||
|
winnersCount: claimedWinnersCount,
|
||||||
|
reserveWinnersCount: claimedReserveCount,
|
||||||
|
seed,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
replayError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replayed = executeDeterministicDrawV1({
|
const winnersMatch = !replayError && replayed !== null
|
||||||
giveawayId: snapshot.giveawayId,
|
|
||||||
snapshot,
|
|
||||||
totalLoadedCount: snapshot.participantCount,
|
|
||||||
winnersCount: claimedWinnersCount,
|
|
||||||
reserveWinnersCount: claimedReserveCount,
|
|
||||||
seed,
|
|
||||||
});
|
|
||||||
|
|
||||||
const winnersMatch = claimedWinnerIds
|
|
||||||
? JSON.stringify(replayed.winnerIds) === JSON.stringify(claimedWinnerIds)
|
? JSON.stringify(replayed.winnerIds) === JSON.stringify(claimedWinnerIds)
|
||||||
: true;
|
: false;
|
||||||
|
|
||||||
const deterministicProofHashMatch = claimedDeterministicProofHash
|
const reserveWinnersMatch = !replayError && replayed !== null
|
||||||
|
? JSON.stringify(replayed.reserveWinnerIds) === JSON.stringify(claimedReserveWinnerIds)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const deterministicProofHashMatch = !replayError && replayed !== null
|
||||||
? replayed.deterministicProofHash === claimedDeterministicProofHash
|
? replayed.deterministicProofHash === claimedDeterministicProofHash
|
||||||
: true;
|
: false;
|
||||||
|
|
||||||
const snapshotHashMatch = replayed.participantsSnapshotHash === snapshot.participantsSnapshotHash;
|
// 4. Check Audit Event Hash: recomputed from giveawayId, drawId, drawnAt, and proof hash
|
||||||
const conditionsHashMatch = replayed.conditionsHash === snapshot.conditionsHash;
|
const expectedAuditEventHash = (!replayError && replayed !== null)
|
||||||
|
? computeAuditEventHash({
|
||||||
|
giveawayId,
|
||||||
|
drawId,
|
||||||
|
drawnAt,
|
||||||
|
deterministicProofHash: replayed.deterministicProofHash,
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
|
||||||
const verified = winnersMatch && deterministicProofHashMatch && snapshotHashMatch && conditionsHashMatch;
|
const auditEventHashMatch = (expectedAuditEventHash === claimedAuditEventHash);
|
||||||
|
|
||||||
|
const verified = (
|
||||||
|
algorithmSupported &&
|
||||||
|
participantsSnapshotIntegrity &&
|
||||||
|
conditionsIntegrity &&
|
||||||
|
winnersMatch &&
|
||||||
|
reserveWinnersMatch &&
|
||||||
|
deterministicProofHashMatch &&
|
||||||
|
auditEventHashMatch
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
verified,
|
verified,
|
||||||
algorithmVersion: ALGORITHM_VERSION_V1,
|
algorithmVersion,
|
||||||
|
algorithmSupported,
|
||||||
|
participantsSnapshotIntegrity,
|
||||||
|
conditionsIntegrity,
|
||||||
winnersMatch,
|
winnersMatch,
|
||||||
snapshotHashMatch,
|
reserveWinnersMatch,
|
||||||
conditionsHashMatch,
|
|
||||||
deterministicProofHashMatch,
|
deterministicProofHashMatch,
|
||||||
expectedWinners: replayed.winners,
|
auditEventHashMatch,
|
||||||
expectedReserveWinners: replayed.reserveWinners,
|
expectedWinners: replayed?.winners || [],
|
||||||
expectedWinnerIds: replayed.winnerIds,
|
expectedReserveWinners: replayed?.reserveWinners || [],
|
||||||
expectedReserveWinnerIds: replayed.reserveWinnerIds,
|
expectedWinnerIds: replayed?.winnerIds || [],
|
||||||
expectedDeterministicProofHash: replayed.deterministicProofHash,
|
expectedReserveWinnerIds: replayed?.reserveWinnerIds || [],
|
||||||
actualDeterministicProofHash: claimedDeterministicProofHash || replayed.deterministicProofHash,
|
expectedDeterministicProofHash: replayed?.deterministicProofHash || '',
|
||||||
|
expectedAuditEventHash,
|
||||||
|
actualDeterministicProofHash: claimedDeterministicProofHash,
|
||||||
|
actualAuditEventHash: claimedAuditEventHash,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export interface ParticipantSnapshotData {
|
||||||
version: number;
|
version: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
eligibleParticipants: FilteredParticipant[];
|
eligibleParticipants: FilteredParticipant[];
|
||||||
|
filterRulesSnapshot: FilterRules;
|
||||||
participantCount: number;
|
participantCount: number;
|
||||||
participantsSnapshotHash: string;
|
participantsSnapshotHash: string;
|
||||||
conditionsHash: string;
|
conditionsHash: string;
|
||||||
|
|
@ -62,17 +63,37 @@ export interface AuditRecordData {
|
||||||
verifiedAt: string;
|
verifiedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VerificationParams {
|
||||||
|
giveawayId: string;
|
||||||
|
drawId: string;
|
||||||
|
drawnAt: string;
|
||||||
|
snapshot: ParticipantSnapshotData;
|
||||||
|
seed: string;
|
||||||
|
claimedWinnersCount: number;
|
||||||
|
claimedReserveCount: number;
|
||||||
|
claimedWinnerIds: string[];
|
||||||
|
claimedReserveWinnerIds: string[];
|
||||||
|
claimedDeterministicProofHash: string;
|
||||||
|
claimedAuditEventHash: string;
|
||||||
|
algorithmVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VerificationResult {
|
export interface VerificationResult {
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
algorithmVersion: string;
|
algorithmVersion: string;
|
||||||
|
algorithmSupported: boolean;
|
||||||
|
participantsSnapshotIntegrity: boolean;
|
||||||
|
conditionsIntegrity: boolean;
|
||||||
winnersMatch: boolean;
|
winnersMatch: boolean;
|
||||||
snapshotHashMatch: boolean;
|
reserveWinnersMatch: boolean;
|
||||||
conditionsHashMatch: boolean;
|
|
||||||
deterministicProofHashMatch: boolean;
|
deterministicProofHashMatch: boolean;
|
||||||
|
auditEventHashMatch: boolean;
|
||||||
expectedWinners: Winner[];
|
expectedWinners: Winner[];
|
||||||
expectedReserveWinners: Winner[];
|
expectedReserveWinners: Winner[];
|
||||||
expectedWinnerIds: string[];
|
expectedWinnerIds: string[];
|
||||||
expectedReserveWinnerIds: string[];
|
expectedReserveWinnerIds: string[];
|
||||||
expectedDeterministicProofHash: string;
|
expectedDeterministicProofHash: string;
|
||||||
|
expectedAuditEventHash: string;
|
||||||
actualDeterministicProofHash: string;
|
actualDeterministicProofHash: string;
|
||||||
|
actualAuditEventHash: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ export class MemoryGiveawayRepository implements IGiveawayRepository {
|
||||||
version: newVersion,
|
version: newVersion,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
eligibleParticipants: [...eligibleParticipants],
|
eligibleParticipants: [...eligibleParticipants],
|
||||||
|
filterRulesSnapshot: { ...rules },
|
||||||
participantCount: eligibleParticipants.length,
|
participantCount: eligibleParticipants.length,
|
||||||
participantsSnapshotHash,
|
participantsSnapshotHash,
|
||||||
conditionsHash,
|
conditionsHash,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
version: s.version,
|
version: s.version,
|
||||||
createdAt: s.createdAt.toISOString(),
|
createdAt: s.createdAt.toISOString(),
|
||||||
eligibleParticipants: s.eligibleParticipants as FilteredParticipant[],
|
eligibleParticipants: s.eligibleParticipants as FilteredParticipant[],
|
||||||
|
filterRulesSnapshot: s.filterRulesSnapshot as FilterRules,
|
||||||
participantCount: s.participantCount,
|
participantCount: s.participantCount,
|
||||||
participantsSnapshotHash: s.participantsSnapshotHash,
|
participantsSnapshotHash: s.participantsSnapshotHash,
|
||||||
conditionsHash: s.conditionsHash,
|
conditionsHash: s.conditionsHash,
|
||||||
|
|
@ -54,7 +55,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
: snapshots.find(s => s.id === raw.drawResult.snapshotId) || latestSnapshot;
|
: snapshots.find(s => s.id === raw.drawResult.snapshotId) || latestSnapshot;
|
||||||
|
|
||||||
drawResult = {
|
drawResult = {
|
||||||
drawId: raw.drawResult.id,
|
drawId: raw.drawResult.drawId || raw.drawResult.id,
|
||||||
giveawayId: raw.drawResult.giveawayId,
|
giveawayId: raw.drawResult.giveawayId,
|
||||||
snapshotId: raw.drawResult.snapshotId,
|
snapshotId: raw.drawResult.snapshotId,
|
||||||
winners: raw.drawResult.winners as any,
|
winners: raw.drawResult.winners as any,
|
||||||
|
|
@ -256,6 +257,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
giveawayId: id,
|
giveawayId: id,
|
||||||
version: newVersion,
|
version: newVersion,
|
||||||
eligibleParticipants: eligibleParticipants as any,
|
eligibleParticipants: eligibleParticipants as any,
|
||||||
|
filterRulesSnapshot: rules as any,
|
||||||
participantCount: eligibleParticipants.length,
|
participantCount: eligibleParticipants.length,
|
||||||
participantsSnapshotHash,
|
participantsSnapshotHash,
|
||||||
conditionsHash,
|
conditionsHash,
|
||||||
|
|
@ -276,6 +278,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
version: snapshot.version,
|
version: snapshot.version,
|
||||||
createdAt: snapshot.createdAt.toISOString(),
|
createdAt: snapshot.createdAt.toISOString(),
|
||||||
eligibleParticipants: eligibleParticipants,
|
eligibleParticipants: eligibleParticipants,
|
||||||
|
filterRulesSnapshot: rules,
|
||||||
participantCount: snapshot.participantCount,
|
participantCount: snapshot.participantCount,
|
||||||
participantsSnapshotHash: snapshot.participantsSnapshotHash,
|
participantsSnapshotHash: snapshot.participantsSnapshotHash,
|
||||||
conditionsHash: snapshot.conditionsHash,
|
conditionsHash: snapshot.conditionsHash,
|
||||||
|
|
@ -296,6 +299,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
version: snap.version,
|
version: snap.version,
|
||||||
createdAt: snap.createdAt.toISOString(),
|
createdAt: snap.createdAt.toISOString(),
|
||||||
eligibleParticipants: snap.eligibleParticipants as any,
|
eligibleParticipants: snap.eligibleParticipants as any,
|
||||||
|
filterRulesSnapshot: snap.filterRulesSnapshot as any,
|
||||||
participantCount: snap.participantCount,
|
participantCount: snap.participantCount,
|
||||||
participantsSnapshotHash: snap.participantsSnapshotHash,
|
participantsSnapshotHash: snap.participantsSnapshotHash,
|
||||||
conditionsHash: snap.conditionsHash,
|
conditionsHash: snap.conditionsHash,
|
||||||
|
|
@ -313,9 +317,10 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
GiveawayFSM.assertCanDraw(current.status);
|
GiveawayFSM.assertCanDraw(current.status);
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
// 1. Create DrawResult
|
// 1. Create DrawResult with original drawId
|
||||||
await tx.drawResult.create({
|
await tx.drawResult.create({
|
||||||
data: {
|
data: {
|
||||||
|
drawId: result.drawId,
|
||||||
giveawayId: id,
|
giveawayId: id,
|
||||||
snapshotId: snapshotId,
|
snapshotId: snapshotId,
|
||||||
winners: result.winners as any,
|
winners: result.winners as any,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { executeDeterministicDrawV1, verifyDrawResult } from '../src/core/randomizer/deterministic';
|
import { executeDeterministicDrawV1, verifyDrawResult } from '../src/core/randomizer/deterministic';
|
||||||
import { computeParticipantsSnapshotHash, computeConditionsHash } from '../src/core/randomizer/canonical';
|
import { computeParticipantsSnapshotHash, computeConditionsHash } from '../src/core/randomizer/canonical';
|
||||||
import { FilterRules, DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
||||||
import { FilteredParticipant } from '../src/core/types/participant';
|
import { FilteredParticipant } from '../src/core/types/participant';
|
||||||
import { ParticipantSnapshotData } from '../src/core/types/audit';
|
import { ParticipantSnapshotData } from '../src/core/types/audit';
|
||||||
|
|
||||||
|
|
@ -54,6 +54,7 @@ describe('DeterministicProofHash & AuditEventHash Separation', () => {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: '2026-08-17T12:00:00.000Z',
|
createdAt: '2026-08-17T12:00:00.000Z',
|
||||||
eligibleParticipants: participants,
|
eligibleParticipants: participants,
|
||||||
|
filterRulesSnapshot: { ...DEFAULT_FILTER_RULES },
|
||||||
participantCount: 3,
|
participantCount: 3,
|
||||||
participantsSnapshotHash: computeParticipantsSnapshotHash(participants),
|
participantsSnapshotHash: computeParticipantsSnapshotHash(participants),
|
||||||
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
|
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
|
||||||
|
|
@ -120,20 +121,28 @@ describe('DeterministicProofHash & AuditEventHash Separation', () => {
|
||||||
seed,
|
seed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const verification = verifyDrawResult(
|
const verification = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-1',
|
||||||
|
drawId: originalDraw.drawId,
|
||||||
|
drawnAt: originalDraw.drawnAt,
|
||||||
snapshot,
|
snapshot,
|
||||||
seed,
|
seed,
|
||||||
1,
|
claimedWinnersCount: 1,
|
||||||
1,
|
claimedReserveCount: 1,
|
||||||
originalDraw.winnerIds,
|
claimedWinnerIds: originalDraw.winnerIds,
|
||||||
originalDraw.deterministicProofHash
|
claimedReserveWinnerIds: originalDraw.reserveWinnerIds,
|
||||||
);
|
claimedDeterministicProofHash: originalDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: originalDraw.auditEventHash,
|
||||||
|
algorithmVersion: originalDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
expect(verification.verified).toBe(true);
|
expect(verification.verified).toBe(true);
|
||||||
expect(verification.winnersMatch).toBe(true);
|
expect(verification.winnersMatch).toBe(true);
|
||||||
|
expect(verification.reserveWinnersMatch).toBe(true);
|
||||||
expect(verification.deterministicProofHashMatch).toBe(true);
|
expect(verification.deterministicProofHashMatch).toBe(true);
|
||||||
expect(verification.snapshotHashMatch).toBe(true);
|
expect(verification.auditEventHashMatch).toBe(true);
|
||||||
expect(verification.conditionsHashMatch).toBe(true);
|
expect(verification.participantsSnapshotIntegrity).toBe(true);
|
||||||
|
expect(verification.conditionsIntegrity).toBe(true);
|
||||||
expect(verification.expectedDeterministicProofHash).toBe(originalDraw.deterministicProofHash);
|
expect(verification.expectedDeterministicProofHash).toBe(originalDraw.deterministicProofHash);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ function createMockSnapshot(count: number): ParticipantSnapshotData {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
eligibleParticipants: eligible,
|
eligibleParticipants: eligible,
|
||||||
|
filterRulesSnapshot: { ...DEFAULT_FILTER_RULES },
|
||||||
participantCount: count,
|
participantCount: count,
|
||||||
participantsSnapshotHash: computeParticipantsSnapshotHash(eligible),
|
participantsSnapshotHash: computeParticipantsSnapshotHash(eligible),
|
||||||
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
|
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
|
||||||
|
|
@ -162,15 +163,20 @@ describe('Deterministic Randomizer V1 (HMAC_SHA256_FY_V1)', () => {
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
});
|
});
|
||||||
|
|
||||||
const verification = verifyDrawResult(
|
const verification = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-audit',
|
||||||
|
drawId: originalDraw.drawId,
|
||||||
|
drawnAt: originalDraw.drawnAt,
|
||||||
snapshot,
|
snapshot,
|
||||||
seed,
|
seed,
|
||||||
2,
|
claimedWinnersCount: 2,
|
||||||
2,
|
claimedReserveCount: 2,
|
||||||
originalDraw.winnerIds,
|
claimedWinnerIds: originalDraw.winnerIds,
|
||||||
originalDraw.deterministicProofHash,
|
claimedReserveWinnerIds: originalDraw.reserveWinnerIds,
|
||||||
ALGORITHM_VERSION_V1
|
claimedDeterministicProofHash: originalDraw.deterministicProofHash,
|
||||||
);
|
claimedAuditEventHash: originalDraw.auditEventHash,
|
||||||
|
algorithmVersion: ALGORITHM_VERSION_V1,
|
||||||
|
});
|
||||||
|
|
||||||
expect(verification.verified).toBe(true);
|
expect(verification.verified).toBe(true);
|
||||||
expect(verification.expectedWinnerIds).toEqual(originalDraw.winnerIds);
|
expect(verification.expectedWinnerIds).toEqual(originalDraw.winnerIds);
|
||||||
|
|
|
||||||
278
tests/tampering-verification.test.ts
Normal file
278
tests/tampering-verification.test.ts
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { executeDeterministicDrawV1, verifyDrawResult } from '../src/core/randomizer/deterministic';
|
||||||
|
import { computeParticipantsSnapshotHash, computeConditionsHash } from '../src/core/randomizer/canonical';
|
||||||
|
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
||||||
|
import { FilteredParticipant } from '../src/core/types/participant';
|
||||||
|
import { ParticipantSnapshotData } from '../src/core/types/audit';
|
||||||
|
|
||||||
|
describe('Public Verification Integrity & Anti-Tampering Test Suite', () => {
|
||||||
|
const originalParticipants: FilteredParticipant[] = [
|
||||||
|
{
|
||||||
|
platformUserId: '101',
|
||||||
|
firstName: 'Иван',
|
||||||
|
lastName: 'Петров',
|
||||||
|
source: 'LIKES',
|
||||||
|
liked: true,
|
||||||
|
commented: false,
|
||||||
|
commentsCount: 0,
|
||||||
|
reposted: false,
|
||||||
|
subscribed: true,
|
||||||
|
eligible: true,
|
||||||
|
exclusionReason: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platformUserId: '102',
|
||||||
|
firstName: 'Анна',
|
||||||
|
lastName: 'Сидорова',
|
||||||
|
source: 'LIKES',
|
||||||
|
liked: true,
|
||||||
|
commented: false,
|
||||||
|
commentsCount: 0,
|
||||||
|
reposted: false,
|
||||||
|
subscribed: true,
|
||||||
|
eligible: true,
|
||||||
|
exclusionReason: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platformUserId: '103',
|
||||||
|
firstName: 'Сергей',
|
||||||
|
lastName: 'Смирнов',
|
||||||
|
source: 'LIKES',
|
||||||
|
liked: true,
|
||||||
|
commented: false,
|
||||||
|
commentsCount: 0,
|
||||||
|
reposted: false,
|
||||||
|
subscribed: true,
|
||||||
|
eligible: true,
|
||||||
|
exclusionReason: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const validSnapshot: ParticipantSnapshotData = {
|
||||||
|
id: 'snap-tamper-baseline',
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
version: 1,
|
||||||
|
createdAt: '2026-08-18T00:00:00.000Z',
|
||||||
|
eligibleParticipants: JSON.parse(JSON.stringify(originalParticipants)),
|
||||||
|
filterRulesSnapshot: { ...DEFAULT_FILTER_RULES },
|
||||||
|
participantCount: 3,
|
||||||
|
participantsSnapshotHash: computeParticipantsSnapshotHash(originalParticipants),
|
||||||
|
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
|
||||||
|
};
|
||||||
|
|
||||||
|
const seed = 'anti-tampering-master-seed-2026';
|
||||||
|
|
||||||
|
const baselineDraw = executeDeterministicDrawV1({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
snapshot: validSnapshot,
|
||||||
|
totalLoadedCount: 3,
|
||||||
|
winnersCount: 1,
|
||||||
|
reserveWinnersCount: 1,
|
||||||
|
seed,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('1. Baseline check: authentic draw result must pass 100% verification', () => {
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: baselineDraw.drawId,
|
||||||
|
drawnAt: baselineDraw.drawnAt,
|
||||||
|
snapshot: validSnapshot,
|
||||||
|
seed,
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: baselineDraw.winnerIds,
|
||||||
|
claimedReserveWinnerIds: baselineDraw.reserveWinnerIds,
|
||||||
|
claimedDeterministicProofHash: baselineDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(true);
|
||||||
|
expect(result.participantsSnapshotIntegrity).toBe(true);
|
||||||
|
expect(result.conditionsIntegrity).toBe(true);
|
||||||
|
expect(result.winnersMatch).toBe(true);
|
||||||
|
expect(result.reserveWinnersMatch).toBe(true);
|
||||||
|
expect(result.deterministicProofHashMatch).toBe(true);
|
||||||
|
expect(result.auditEventHashMatch).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2. Tampering test: modifying a participant name/ID in snapshot must fail participantsSnapshotIntegrity', () => {
|
||||||
|
const tamperedSnapshot: ParticipantSnapshotData = {
|
||||||
|
...validSnapshot,
|
||||||
|
eligibleParticipants: [
|
||||||
|
{
|
||||||
|
...originalParticipants[0],
|
||||||
|
firstName: 'Хакер', // Tampered name!
|
||||||
|
},
|
||||||
|
originalParticipants[1],
|
||||||
|
originalParticipants[2],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: baselineDraw.drawId,
|
||||||
|
drawnAt: baselineDraw.drawnAt,
|
||||||
|
snapshot: tamperedSnapshot,
|
||||||
|
seed,
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: baselineDraw.winnerIds,
|
||||||
|
claimedReserveWinnerIds: baselineDraw.reserveWinnerIds,
|
||||||
|
claimedDeterministicProofHash: baselineDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(false);
|
||||||
|
expect(result.participantsSnapshotIntegrity).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('3. Tampering test: modifying a filter rule in snapshot must fail conditionsIntegrity', () => {
|
||||||
|
const tamperedSnapshot: ParticipantSnapshotData = {
|
||||||
|
...validSnapshot,
|
||||||
|
filterRulesSnapshot: {
|
||||||
|
...DEFAULT_FILTER_RULES,
|
||||||
|
requireComment: true, // Tampered rule!
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: baselineDraw.drawId,
|
||||||
|
drawnAt: baselineDraw.drawnAt,
|
||||||
|
snapshot: tamperedSnapshot,
|
||||||
|
seed,
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: baselineDraw.winnerIds,
|
||||||
|
claimedReserveWinnerIds: baselineDraw.reserveWinnerIds,
|
||||||
|
claimedDeterministicProofHash: baselineDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(false);
|
||||||
|
expect(result.conditionsIntegrity).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('4. Tampering test: modifying winnerIds must fail winnersMatch', () => {
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: baselineDraw.drawId,
|
||||||
|
drawnAt: baselineDraw.drawnAt,
|
||||||
|
snapshot: validSnapshot,
|
||||||
|
seed,
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: ['fake-winner-id-999'], // Tampered winner!
|
||||||
|
claimedReserveWinnerIds: baselineDraw.reserveWinnerIds,
|
||||||
|
claimedDeterministicProofHash: baselineDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(false);
|
||||||
|
expect(result.winnersMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('5. Tampering test: modifying reserveWinnerIds must fail reserveWinnersMatch', () => {
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: baselineDraw.drawId,
|
||||||
|
drawnAt: baselineDraw.drawnAt,
|
||||||
|
snapshot: validSnapshot,
|
||||||
|
seed,
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: baselineDraw.winnerIds,
|
||||||
|
claimedReserveWinnerIds: ['fake-reserve-id-777'], // Tampered reserve winner!
|
||||||
|
claimedDeterministicProofHash: baselineDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(false);
|
||||||
|
expect(result.reserveWinnersMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('6. Tampering test: modifying seed must fail replay and deterministicProofHashMatch', () => {
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: baselineDraw.drawId,
|
||||||
|
drawnAt: baselineDraw.drawnAt,
|
||||||
|
snapshot: validSnapshot,
|
||||||
|
seed: 'tampered-seed-999',
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: baselineDraw.winnerIds,
|
||||||
|
claimedReserveWinnerIds: baselineDraw.reserveWinnerIds,
|
||||||
|
claimedDeterministicProofHash: baselineDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(false);
|
||||||
|
expect(result.deterministicProofHashMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('7. Tampering test: modifying drawId must fail auditEventHashMatch', () => {
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: 'tampered-draw-id-xyz', // Tampered drawId!
|
||||||
|
drawnAt: baselineDraw.drawnAt,
|
||||||
|
snapshot: validSnapshot,
|
||||||
|
seed,
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: baselineDraw.winnerIds,
|
||||||
|
claimedReserveWinnerIds: baselineDraw.reserveWinnerIds,
|
||||||
|
claimedDeterministicProofHash: baselineDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(false);
|
||||||
|
expect(result.auditEventHashMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('8. Tampering test: modifying drawnAt timestamp must fail auditEventHashMatch', () => {
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: baselineDraw.drawId,
|
||||||
|
drawnAt: '2026-08-19T00:00:00.000Z', // Tampered timestamp!
|
||||||
|
snapshot: validSnapshot,
|
||||||
|
seed,
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: baselineDraw.winnerIds,
|
||||||
|
claimedReserveWinnerIds: baselineDraw.reserveWinnerIds,
|
||||||
|
claimedDeterministicProofHash: baselineDraw.deterministicProofHash,
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(false);
|
||||||
|
expect(result.auditEventHashMatch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('9. Tampering test: modifying deterministicProofHash directly must fail deterministicProofHashMatch', () => {
|
||||||
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-tamper-1',
|
||||||
|
drawId: baselineDraw.drawId,
|
||||||
|
drawnAt: baselineDraw.drawnAt,
|
||||||
|
snapshot: validSnapshot,
|
||||||
|
seed,
|
||||||
|
claimedWinnersCount: 1,
|
||||||
|
claimedReserveCount: 1,
|
||||||
|
claimedWinnerIds: baselineDraw.winnerIds,
|
||||||
|
claimedReserveWinnerIds: baselineDraw.reserveWinnerIds,
|
||||||
|
claimedDeterministicProofHash: '1111111111111111111111111111111111111111111111111111111111111111',
|
||||||
|
claimedAuditEventHash: baselineDraw.auditEventHash,
|
||||||
|
algorithmVersion: baselineDraw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.verified).toBe(false);
|
||||||
|
expect(result.deterministicProofHashMatch).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -54,6 +54,7 @@ describe('Verification API Replay Engine', () => {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: '2026-08-17T12:00:00.000Z',
|
createdAt: '2026-08-17T12:00:00.000Z',
|
||||||
eligibleParticipants: participants,
|
eligibleParticipants: participants,
|
||||||
|
filterRulesSnapshot: { ...DEFAULT_FILTER_RULES },
|
||||||
participantCount: 3,
|
participantCount: 3,
|
||||||
participantsSnapshotHash: computeParticipantsSnapshotHash(participants),
|
participantsSnapshotHash: computeParticipantsSnapshotHash(participants),
|
||||||
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
|
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
|
||||||
|
|
@ -71,21 +72,28 @@ describe('Verification API Replay Engine', () => {
|
||||||
seed,
|
seed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = verifyDrawResult(
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-verif-1',
|
||||||
|
drawId: draw.drawId,
|
||||||
|
drawnAt: draw.drawnAt,
|
||||||
snapshot,
|
snapshot,
|
||||||
seed,
|
seed,
|
||||||
1,
|
claimedWinnersCount: 1,
|
||||||
1,
|
claimedReserveCount: 1,
|
||||||
draw.winnerIds,
|
claimedWinnerIds: draw.winnerIds,
|
||||||
draw.deterministicProofHash,
|
claimedReserveWinnerIds: draw.reserveWinnerIds,
|
||||||
draw.algorithmVersion
|
claimedDeterministicProofHash: draw.deterministicProofHash,
|
||||||
);
|
claimedAuditEventHash: draw.auditEventHash,
|
||||||
|
algorithmVersion: draw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.verified).toBe(true);
|
expect(result.verified).toBe(true);
|
||||||
expect(result.winnersMatch).toBe(true);
|
expect(result.winnersMatch).toBe(true);
|
||||||
|
expect(result.reserveWinnersMatch).toBe(true);
|
||||||
expect(result.deterministicProofHashMatch).toBe(true);
|
expect(result.deterministicProofHashMatch).toBe(true);
|
||||||
expect(result.snapshotHashMatch).toBe(true);
|
expect(result.auditEventHashMatch).toBe(true);
|
||||||
expect(result.conditionsHashMatch).toBe(true);
|
expect(result.participantsSnapshotIntegrity).toBe(true);
|
||||||
|
expect(result.conditionsIntegrity).toBe(true);
|
||||||
expect(result.expectedWinnerIds).toEqual(draw.winnerIds);
|
expect(result.expectedWinnerIds).toEqual(draw.winnerIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,15 +109,20 @@ describe('Verification API Replay Engine', () => {
|
||||||
|
|
||||||
const fakeWinnerIds = ['9999']; // Tampered winners
|
const fakeWinnerIds = ['9999']; // Tampered winners
|
||||||
|
|
||||||
const result = verifyDrawResult(
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-verif-1',
|
||||||
|
drawId: draw.drawId,
|
||||||
|
drawnAt: draw.drawnAt,
|
||||||
snapshot,
|
snapshot,
|
||||||
seed,
|
seed,
|
||||||
1,
|
claimedWinnersCount: 1,
|
||||||
1,
|
claimedReserveCount: 1,
|
||||||
fakeWinnerIds,
|
claimedWinnerIds: fakeWinnerIds,
|
||||||
draw.deterministicProofHash,
|
claimedReserveWinnerIds: draw.reserveWinnerIds,
|
||||||
draw.algorithmVersion
|
claimedDeterministicProofHash: draw.deterministicProofHash,
|
||||||
);
|
claimedAuditEventHash: draw.auditEventHash,
|
||||||
|
algorithmVersion: draw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.verified).toBe(false);
|
expect(result.verified).toBe(false);
|
||||||
expect(result.winnersMatch).toBe(false);
|
expect(result.winnersMatch).toBe(false);
|
||||||
|
|
@ -127,15 +140,20 @@ describe('Verification API Replay Engine', () => {
|
||||||
|
|
||||||
const fakeProofHash = '0000000000000000000000000000000000000000000000000000000000000000';
|
const fakeProofHash = '0000000000000000000000000000000000000000000000000000000000000000';
|
||||||
|
|
||||||
const result = verifyDrawResult(
|
const result = verifyDrawResult({
|
||||||
|
giveawayId: 'gw-verif-1',
|
||||||
|
drawId: draw.drawId,
|
||||||
|
drawnAt: draw.drawnAt,
|
||||||
snapshot,
|
snapshot,
|
||||||
seed,
|
seed,
|
||||||
1,
|
claimedWinnersCount: 1,
|
||||||
1,
|
claimedReserveCount: 1,
|
||||||
draw.winnerIds,
|
claimedWinnerIds: draw.winnerIds,
|
||||||
fakeProofHash,
|
claimedReserveWinnerIds: draw.reserveWinnerIds,
|
||||||
draw.algorithmVersion
|
claimedDeterministicProofHash: fakeProofHash,
|
||||||
);
|
claimedAuditEventHash: draw.auditEventHash,
|
||||||
|
algorithmVersion: draw.algorithmVersion,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.verified).toBe(false);
|
expect(result.verified).toBe(false);
|
||||||
expect(result.deterministicProofHashMatch).toBe(false);
|
expect(result.deterministicProofHashMatch).toBe(false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue