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:
Ochenstarik 2026-08-18 00:15:30 +07:00
parent 1bc6650041
commit 26e82fcf8d
11 changed files with 511 additions and 97 deletions

View file

@ -89,6 +89,7 @@ model ParticipantSnapshot {
version Int @default(1)
createdAt DateTime @default(now())
eligibleParticipants Json // Canonical JSON array of FilteredParticipant
filterRulesSnapshot Json // Canonical JSON snapshot of FilterRules
participantCount Int
participantsSnapshotHash String
conditionsHash String
@ -102,6 +103,7 @@ model ParticipantSnapshot {
model DrawResult {
id String @id @default(cuid())
drawId String // Original domain drawId (e.g. draw_8f9102ab...)
giveawayId String @unique
giveaway Giveaway @relation(fields: [giveawayId], references: [id], onDelete: Cascade)
snapshotId String @unique

View file

@ -21,13 +21,14 @@ export async function GET(
}, { status: 400 });
}
// Find the snapshot associated with this draw
const snapshot = giveaway.snapshots.find(s => s.id === drawResult.snapshotId)
|| giveaway.latestSnapshot;
// Strict snapshot lookup: DO NOT fallback to latestSnapshot
const snapshot = giveaway.snapshots.find(s => s.id === drawResult.snapshotId);
if (!snapshot) {
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 });
}
@ -35,29 +36,38 @@ export async function GET(
const claimedReserveCount = drawResult.reserveWinners.length;
// Run independent cryptographic replay verification
const verification = verifyDrawResult(
const verification = verifyDrawResult({
giveawayId: id,
drawId: drawResult.drawId,
drawnAt: drawResult.drawnAt,
snapshot,
drawResult.seedUsed,
seed: drawResult.seedUsed,
claimedWinnersCount,
claimedReserveCount,
drawResult.winnerIds,
drawResult.deterministicProofHash,
drawResult.algorithmVersion
);
claimedWinnerIds: drawResult.winnerIds,
claimedReserveWinnerIds: drawResult.reserveWinnerIds,
claimedDeterministicProofHash: drawResult.deterministicProofHash,
claimedAuditEventHash: drawResult.auditEventHash,
algorithmVersion: drawResult.algorithmVersion,
});
return NextResponse.json({
verified: verification.verified,
giveawayId: id,
drawId: drawResult.drawId,
snapshotId: snapshot.id,
algorithmVersion: verification.algorithmVersion,
algorithmSupported: verification.algorithmSupported,
participantsSnapshotIntegrity: verification.participantsSnapshotIntegrity,
conditionsIntegrity: verification.conditionsIntegrity,
winnersMatch: verification.winnersMatch,
snapshotHashMatch: verification.snapshotHashMatch,
conditionsHashMatch: verification.conditionsHashMatch,
reserveWinnersMatch: verification.reserveWinnersMatch,
deterministicProofHashMatch: verification.deterministicProofHashMatch,
auditEventHashMatch: verification.auditEventHashMatch,
expectedWinnerIds: verification.expectedWinnerIds,
expectedReserveWinnerIds: verification.expectedReserveWinnerIds,
deterministicProofHash: verification.expectedDeterministicProofHash,
auditEventHash: drawResult.auditEventHash,
auditEventHash: verification.expectedAuditEventHash,
drawnAt: drawResult.drawnAt,
});
} catch (error: any) {

View file

@ -201,6 +201,7 @@ export default function GiveawayDetailPage() {
onClick={() => {
navigator.clipboard.writeText(JSON.stringify({
giveawayId: giveaway.id,
drawId: drawResult.drawId,
snapshotId: drawResult.snapshotId,
algorithmVersion: drawResult.algorithmVersion,
seed: drawResult.seedUsed,
@ -243,16 +244,22 @@ export default function GiveawayDetailPage() {
</>
)}
</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.snapshotHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
<div>Хеш условий совпал: {verificationResult.conditionsHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
<div>Резерв совпал: {verificationResult.reserveWinnersMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
<div>Proof Hash совпал: {verificationResult.deterministicProofHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
<div>Event Hash совпал: {verificationResult.auditEventHashMatch ? 'ДА ✓' : 'НЕТ ✗'}</div>
</div>
</div>
)}
<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">
<span className="text-slate-400">Snapshot ID:</span>
<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>
<p className="font-mono text-emerald-400 break-all">{drawResult.participantsSnapshotHash}</p>
</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">
<span className="text-slate-400">deterministicProofHash (воспроизводимый):</span>
<p className="font-mono text-indigo-300 break-all">{drawResult.deterministicProofHash}</p>

View file

@ -5,10 +5,16 @@ import {
DrawExecutionResult,
ALGORITHM_HMAC_SHA256_FY_V1,
ParticipantSnapshotData,
VerificationParams,
VerificationResult
} from '../types/audit';
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'
@ -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(
snapshot: ParticipantSnapshotData,
seed: string,
claimedWinnersCount: number,
claimedReserveCount: number,
claimedWinnerIds?: string[],
claimedDeterministicProofHash?: string,
algorithmVersion: string = ALGORITHM_VERSION_V1
): VerificationResult {
if (algorithmVersion !== ALGORITHM_VERSION_V1) {
throw new Error(`Unsupported algorithm version for replay: ${algorithmVersion}`);
export function verifyDrawResult(params: VerificationParams): VerificationResult {
const {
giveawayId,
drawId,
drawnAt,
snapshot,
seed,
claimedWinnersCount,
claimedReserveCount,
claimedWinnerIds = [],
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({
giveawayId: snapshot.giveawayId,
snapshot,
totalLoadedCount: snapshot.participantCount,
winnersCount: claimedWinnersCount,
reserveWinnersCount: claimedReserveCount,
seed,
});
const winnersMatch = claimedWinnerIds
const winnersMatch = !replayError && replayed !== null
? 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
: true;
: false;
const snapshotHashMatch = replayed.participantsSnapshotHash === snapshot.participantsSnapshotHash;
const conditionsHashMatch = replayed.conditionsHash === snapshot.conditionsHash;
// 4. Check Audit Event Hash: recomputed from giveawayId, drawId, drawnAt, and proof hash
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 {
verified,
algorithmVersion: ALGORITHM_VERSION_V1,
algorithmVersion,
algorithmSupported,
participantsSnapshotIntegrity,
conditionsIntegrity,
winnersMatch,
snapshotHashMatch,
conditionsHashMatch,
reserveWinnersMatch,
deterministicProofHashMatch,
expectedWinners: replayed.winners,
expectedReserveWinners: replayed.reserveWinners,
expectedWinnerIds: replayed.winnerIds,
expectedReserveWinnerIds: replayed.reserveWinnerIds,
expectedDeterministicProofHash: replayed.deterministicProofHash,
actualDeterministicProofHash: claimedDeterministicProofHash || replayed.deterministicProofHash,
auditEventHashMatch,
expectedWinners: replayed?.winners || [],
expectedReserveWinners: replayed?.reserveWinners || [],
expectedWinnerIds: replayed?.winnerIds || [],
expectedReserveWinnerIds: replayed?.reserveWinnerIds || [],
expectedDeterministicProofHash: replayed?.deterministicProofHash || '',
expectedAuditEventHash,
actualDeterministicProofHash: claimedDeterministicProofHash,
actualAuditEventHash: claimedAuditEventHash,
};
}

View file

@ -10,6 +10,7 @@ export interface ParticipantSnapshotData {
version: number;
createdAt: string;
eligibleParticipants: FilteredParticipant[];
filterRulesSnapshot: FilterRules;
participantCount: number;
participantsSnapshotHash: string;
conditionsHash: string;
@ -62,17 +63,37 @@ export interface AuditRecordData {
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 {
verified: boolean;
algorithmVersion: string;
algorithmSupported: boolean;
participantsSnapshotIntegrity: boolean;
conditionsIntegrity: boolean;
winnersMatch: boolean;
snapshotHashMatch: boolean;
conditionsHashMatch: boolean;
reserveWinnersMatch: boolean;
deterministicProofHashMatch: boolean;
auditEventHashMatch: boolean;
expectedWinners: Winner[];
expectedReserveWinners: Winner[];
expectedWinnerIds: string[];
expectedReserveWinnerIds: string[];
expectedDeterministicProofHash: string;
expectedAuditEventHash: string;
actualDeterministicProofHash: string;
actualAuditEventHash: string;
}

View file

@ -131,6 +131,7 @@ export class MemoryGiveawayRepository implements IGiveawayRepository {
version: newVersion,
createdAt: new Date().toISOString(),
eligibleParticipants: [...eligibleParticipants],
filterRulesSnapshot: { ...rules },
participantCount: eligibleParticipants.length,
participantsSnapshotHash,
conditionsHash,

View file

@ -34,6 +34,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
version: s.version,
createdAt: s.createdAt.toISOString(),
eligibleParticipants: s.eligibleParticipants as FilteredParticipant[],
filterRulesSnapshot: s.filterRulesSnapshot as FilterRules,
participantCount: s.participantCount,
participantsSnapshotHash: s.participantsSnapshotHash,
conditionsHash: s.conditionsHash,
@ -54,7 +55,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
: snapshots.find(s => s.id === raw.drawResult.snapshotId) || latestSnapshot;
drawResult = {
drawId: raw.drawResult.id,
drawId: raw.drawResult.drawId || raw.drawResult.id,
giveawayId: raw.drawResult.giveawayId,
snapshotId: raw.drawResult.snapshotId,
winners: raw.drawResult.winners as any,
@ -256,6 +257,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
giveawayId: id,
version: newVersion,
eligibleParticipants: eligibleParticipants as any,
filterRulesSnapshot: rules as any,
participantCount: eligibleParticipants.length,
participantsSnapshotHash,
conditionsHash,
@ -276,6 +278,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
version: snapshot.version,
createdAt: snapshot.createdAt.toISOString(),
eligibleParticipants: eligibleParticipants,
filterRulesSnapshot: rules,
participantCount: snapshot.participantCount,
participantsSnapshotHash: snapshot.participantsSnapshotHash,
conditionsHash: snapshot.conditionsHash,
@ -296,6 +299,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
version: snap.version,
createdAt: snap.createdAt.toISOString(),
eligibleParticipants: snap.eligibleParticipants as any,
filterRulesSnapshot: snap.filterRulesSnapshot as any,
participantCount: snap.participantCount,
participantsSnapshotHash: snap.participantsSnapshotHash,
conditionsHash: snap.conditionsHash,
@ -313,9 +317,10 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
GiveawayFSM.assertCanDraw(current.status);
await prisma.$transaction(async (tx) => {
// 1. Create DrawResult
// 1. Create DrawResult with original drawId
await tx.drawResult.create({
data: {
drawId: result.drawId,
giveawayId: id,
snapshotId: snapshotId,
winners: result.winners as any,

View file

@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { executeDeterministicDrawV1, verifyDrawResult } from '../src/core/randomizer/deterministic';
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 { ParticipantSnapshotData } from '../src/core/types/audit';
@ -54,6 +54,7 @@ describe('DeterministicProofHash & AuditEventHash Separation', () => {
version: 1,
createdAt: '2026-08-17T12:00:00.000Z',
eligibleParticipants: participants,
filterRulesSnapshot: { ...DEFAULT_FILTER_RULES },
participantCount: 3,
participantsSnapshotHash: computeParticipantsSnapshotHash(participants),
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
@ -120,20 +121,28 @@ describe('DeterministicProofHash & AuditEventHash Separation', () => {
seed,
});
const verification = verifyDrawResult(
const verification = verifyDrawResult({
giveawayId: 'gw-1',
drawId: originalDraw.drawId,
drawnAt: originalDraw.drawnAt,
snapshot,
seed,
1,
1,
originalDraw.winnerIds,
originalDraw.deterministicProofHash
);
claimedWinnersCount: 1,
claimedReserveCount: 1,
claimedWinnerIds: originalDraw.winnerIds,
claimedReserveWinnerIds: originalDraw.reserveWinnerIds,
claimedDeterministicProofHash: originalDraw.deterministicProofHash,
claimedAuditEventHash: originalDraw.auditEventHash,
algorithmVersion: originalDraw.algorithmVersion,
});
expect(verification.verified).toBe(true);
expect(verification.winnersMatch).toBe(true);
expect(verification.reserveWinnersMatch).toBe(true);
expect(verification.deterministicProofHashMatch).toBe(true);
expect(verification.snapshotHashMatch).toBe(true);
expect(verification.conditionsHashMatch).toBe(true);
expect(verification.auditEventHashMatch).toBe(true);
expect(verification.participantsSnapshotIntegrity).toBe(true);
expect(verification.conditionsIntegrity).toBe(true);
expect(verification.expectedDeterministicProofHash).toBe(originalDraw.deterministicProofHash);
});
});

View file

@ -29,6 +29,7 @@ function createMockSnapshot(count: number): ParticipantSnapshotData {
version: 1,
createdAt: new Date().toISOString(),
eligibleParticipants: eligible,
filterRulesSnapshot: { ...DEFAULT_FILTER_RULES },
participantCount: count,
participantsSnapshotHash: computeParticipantsSnapshotHash(eligible),
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
@ -162,15 +163,20 @@ describe('Deterministic Randomizer V1 (HMAC_SHA256_FY_V1)', () => {
filterRules: DEFAULT_FILTER_RULES,
});
const verification = verifyDrawResult(
const verification = verifyDrawResult({
giveawayId: 'gw-audit',
drawId: originalDraw.drawId,
drawnAt: originalDraw.drawnAt,
snapshot,
seed,
2,
2,
originalDraw.winnerIds,
originalDraw.deterministicProofHash,
ALGORITHM_VERSION_V1
);
claimedWinnersCount: 2,
claimedReserveCount: 2,
claimedWinnerIds: originalDraw.winnerIds,
claimedReserveWinnerIds: originalDraw.reserveWinnerIds,
claimedDeterministicProofHash: originalDraw.deterministicProofHash,
claimedAuditEventHash: originalDraw.auditEventHash,
algorithmVersion: ALGORITHM_VERSION_V1,
});
expect(verification.verified).toBe(true);
expect(verification.expectedWinnerIds).toEqual(originalDraw.winnerIds);

View 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);
});
});

View file

@ -54,6 +54,7 @@ describe('Verification API Replay Engine', () => {
version: 1,
createdAt: '2026-08-17T12:00:00.000Z',
eligibleParticipants: participants,
filterRulesSnapshot: { ...DEFAULT_FILTER_RULES },
participantCount: 3,
participantsSnapshotHash: computeParticipantsSnapshotHash(participants),
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
@ -71,21 +72,28 @@ describe('Verification API Replay Engine', () => {
seed,
});
const result = verifyDrawResult(
const result = verifyDrawResult({
giveawayId: 'gw-verif-1',
drawId: draw.drawId,
drawnAt: draw.drawnAt,
snapshot,
seed,
1,
1,
draw.winnerIds,
draw.deterministicProofHash,
draw.algorithmVersion
);
claimedWinnersCount: 1,
claimedReserveCount: 1,
claimedWinnerIds: draw.winnerIds,
claimedReserveWinnerIds: draw.reserveWinnerIds,
claimedDeterministicProofHash: draw.deterministicProofHash,
claimedAuditEventHash: draw.auditEventHash,
algorithmVersion: draw.algorithmVersion,
});
expect(result.verified).toBe(true);
expect(result.winnersMatch).toBe(true);
expect(result.reserveWinnersMatch).toBe(true);
expect(result.deterministicProofHashMatch).toBe(true);
expect(result.snapshotHashMatch).toBe(true);
expect(result.conditionsHashMatch).toBe(true);
expect(result.auditEventHashMatch).toBe(true);
expect(result.participantsSnapshotIntegrity).toBe(true);
expect(result.conditionsIntegrity).toBe(true);
expect(result.expectedWinnerIds).toEqual(draw.winnerIds);
});
@ -101,15 +109,20 @@ describe('Verification API Replay Engine', () => {
const fakeWinnerIds = ['9999']; // Tampered winners
const result = verifyDrawResult(
const result = verifyDrawResult({
giveawayId: 'gw-verif-1',
drawId: draw.drawId,
drawnAt: draw.drawnAt,
snapshot,
seed,
1,
1,
fakeWinnerIds,
draw.deterministicProofHash,
draw.algorithmVersion
);
claimedWinnersCount: 1,
claimedReserveCount: 1,
claimedWinnerIds: fakeWinnerIds,
claimedReserveWinnerIds: draw.reserveWinnerIds,
claimedDeterministicProofHash: draw.deterministicProofHash,
claimedAuditEventHash: draw.auditEventHash,
algorithmVersion: draw.algorithmVersion,
});
expect(result.verified).toBe(false);
expect(result.winnersMatch).toBe(false);
@ -127,15 +140,20 @@ describe('Verification API Replay Engine', () => {
const fakeProofHash = '0000000000000000000000000000000000000000000000000000000000000000';
const result = verifyDrawResult(
const result = verifyDrawResult({
giveawayId: 'gw-verif-1',
drawId: draw.drawId,
drawnAt: draw.drawnAt,
snapshot,
seed,
1,
1,
draw.winnerIds,
fakeProofHash,
draw.algorithmVersion
);
claimedWinnersCount: 1,
claimedReserveCount: 1,
claimedWinnerIds: draw.winnerIds,
claimedReserveWinnerIds: draw.reserveWinnerIds,
claimedDeterministicProofHash: fakeProofHash,
claimedAuditEventHash: draw.auditEventHash,
algorithmVersion: draw.algorithmVersion,
});
expect(result.verified).toBe(false);
expect(result.deterministicProofHashMatch).toBe(false);