From a26b2773dd215b628eb7482854256ed792c9311b Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 28 May 2026 23:12:45 +0000 Subject: [PATCH 1/3] implemented the sybil --- prisma/schema.prisma | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 97b00d9..7623e22 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -63,3 +63,12 @@ model SybilScore { @@index([userId]) @@index([compositeScore]) } + +model SybilExplanation { + id String @id @default(uuid()) + sybilScoreId String @unique + explanation String + createdAt DateTime @default(now()) + + sybilScore SybilScore @relation(fields: [sybilScoreId], references: [id], onDelete: Cascade) +} From 4340e50ad8a716f3c7b7d8c745732856fc025ba3 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 28 May 2026 23:13:20 +0000 Subject: [PATCH 2/3] implemented the sybil --- .../sybil-resistance.service.ts | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/sybil-resistance/sybil-resistance.service.ts b/src/sybil-resistance/sybil-resistance.service.ts index 69ced8a..33fa50e 100644 --- a/src/sybil-resistance/sybil-resistance.service.ts +++ b/src/sybil-resistance/sybil-resistance.service.ts @@ -210,7 +210,12 @@ Final score: ${Number(composite.toFixed(4))} (weighted average)`, async recordSybilScore(userId: string): Promise { const { score: compositeScore, details } = await this.computeSybilScore(userId); - return this.prisma.sybilScore.create({ + // Persist SybilScore without the potentially large `explanation` text + const detailsCopy: any = { ...details }; + const explanationText = detailsCopy.explanation; + delete detailsCopy.explanation; + + const scoreRecord = await this.prisma.sybilScore.create({ data: { userId, worldcoinScore: details.componentScores.worldcoin, @@ -218,9 +223,21 @@ Final score: ${Number(composite.toFixed(4))} (weighted average)`, stakingScore: details.componentScores.staking, accuracyScore: details.componentScores.accuracy, compositeScore, - calculationDetails: JSON.stringify(details), + calculationDetails: JSON.stringify(detailsCopy), }, }); + + // Store explanation separately to avoid huge JSON columns + if (explanationText) { + await this.prisma.sybilExplanation.create({ + data: { + sybilScoreId: scoreRecord.id, + explanation: explanationText, + }, + }); + } + + return scoreRecord; } /** @@ -350,11 +367,25 @@ Final score: ${Number(composite.toFixed(4))} (weighted average)`, }> { const score = await this.getLatestSybilScore(userId); + // Parse calculation details and, if explanation was stored separately, load it + let details = score.calculationDetails ? JSON.parse(score.calculationDetails) : null; + if (details && !details.explanation) { + // try to load explanation from separate table + try { + const expl = await this.prisma.sybilExplanation.findFirst({ where: { sybilScoreId: score.id } }); + if (expl && expl.explanation) { + details.explanation = expl.explanation; + } + } catch (err) { + // ignore missing explanation + } + } + return { userId, score: score.compositeScore, isVerified: score.worldcoinScore > 0, - details: score.calculationDetails ? JSON.parse(score.calculationDetails) : null, + details, }; } } From 7eedf4eab4eca83df825215156827b3eee8c7b49 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 28 May 2026 23:14:41 +0000 Subject: [PATCH 3/3] implemented the sybil --- .../sybil-resistance.service.spec.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/sybil-resistance/sybil-resistance.service.spec.ts b/src/sybil-resistance/sybil-resistance.service.spec.ts index 8046607..eed1e67 100644 --- a/src/sybil-resistance/sybil-resistance.service.spec.ts +++ b/src/sybil-resistance/sybil-resistance.service.spec.ts @@ -42,6 +42,10 @@ describe('SybilResistanceService', () => { findFirst: jest.fn(), findMany: jest.fn(), }, + sybilExplanation: { + create: jest.fn(), + findFirst: jest.fn(), + }, }, }, ], @@ -155,10 +159,14 @@ describe('SybilResistanceService', () => { }; jest.spyOn(prisma.sybilScore, 'create').mockResolvedValueOnce(mockScoreRecord); + jest.spyOn(prisma.sybilExplanation, 'create').mockResolvedValueOnce({ id: 'ex-1', sybilScoreId: 'score-1', explanation: 'exp' }); const result = await service.recordSybilScore(mockUserId); expect(prisma.sybilScore.create).toHaveBeenCalled(); + expect(prisma.sybilExplanation.create).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ sybilScoreId: 'score-1' }) }), + ); expect(result.userId).toBe(mockUserId); expect(result.compositeScore).toBeDefined(); }); @@ -407,6 +415,29 @@ describe('SybilResistanceService', () => { expect(result.details).toBeDefined(); }); + it('should load explanation from SybilExplanation if not present in calculationDetails', async () => { + const mockScore = { + id: 'score-1', + userId: mockUserId, + compositeScore: 0.57, + worldcoinScore: 1.0, + walletAgeScore: 0.67, + stakingScore: 0.0, + accuracyScore: 0.0, + calculationDetails: JSON.stringify({ componentScores: { worldcoin: 1.0 } }), + createdAt: new Date(), + updatedAt: new Date(), + }; + + jest.spyOn(prisma.sybilScore, 'findFirst').mockResolvedValueOnce(mockScore); + jest.spyOn(prisma.sybilExplanation, 'findFirst').mockResolvedValueOnce({ id: 'ex-1', sybilScoreId: 'score-1', explanation: 'Stored explanation' }); + + const result = await service.getSybilScoreForVoting(mockUserId); + + expect(result.details).toBeDefined(); + expect(result.details.explanation).toBe('Stored explanation'); + }); + it('should indicate unverified status correctly', async () => { const mockScore = { id: 'score-1',