Files
social-tw-website/packages/relay/src/services/ReportService.ts

628 lines
21 KiB
TypeScript

import { DB } from 'anondb'
import { Groth16Proof, PublicSignals } from 'snarkjs'
import {
ReportNonNullifierProof,
ReportNullifierProof,
} from '../../../circuits/src'
import { REPORT_SETTLE_VOTE_THRESHOLD } from '../config'
import { commentService } from '../services/CommentService'
import { postService } from '../services/PostService'
import {
AdjudicateValue,
Adjudicator,
Errors,
ReportCategory,
ReportHistory,
ReportStatus,
ReportType,
} from '../types'
import { CommentStatus } from '../types/Comment'
import { PostStatus } from '../types/Post'
import {
ClaimHelpers,
ClaimMethods,
RepChangeType,
RepUserType,
ReputationType,
} from '../types/Reputation'
import { UnirepSocialSynchronizer } from './singletons/UnirepSocialSynchronizer'
import ProofHelper from './utils/ProofHelper'
import TransactionManager from './utils/TransactionManager'
import Validator from './utils/Validator'
export class ReportService {
async verifyReportData(
db: DB,
reportData: ReportHistory,
publicSignals: PublicSignals,
proof: Groth16Proof,
synchronizer: UnirepSocialSynchronizer
): Promise<ReportHistory> {
// 1.a Check if the post / comment exists is not reported already(post status = 1 / comment status = 1)
if (reportData.type === ReportType.POST) {
if (!Validator.isValidNumber(reportData.objectId))
throw Errors.INVALID_POST_ID()
const post = await postService.fetchSinglePost(
reportData.objectId.toString(),
db
)
if (!post) throw Errors.POST_NOT_EXIST()
if (post.status === PostStatus.REPORTED)
throw Errors.POST_REPORTED()
reportData.respondentEpochKey = post.epochKey
} else if (reportData.type === ReportType.COMMENT) {
if (!Validator.isValidNumber(reportData.objectId))
throw Errors.INVALID_COMMENT_ID()
const comment = await commentService.fetchSingleComment(
reportData.objectId.toString(),
db
)
if (!comment) throw Errors.COMMENT_NOT_EXIST()
if (comment.status === CommentStatus.REPORTED)
throw Errors.COMMENT_REPORTED()
reportData.respondentEpochKey = comment.epochKey
} else {
throw Errors.REPORT_OBJECT_TYPE_NOT_EXISTS()
}
// 1.b Check if the epoch key is valid
await ProofHelper.getAndVerifyEpochKeyProof(
publicSignals,
proof,
synchronizer
)
return reportData
}
async createReport(db: DB, reportData: ReportHistory): Promise<string> {
// get the latest reportId
const reportId = await db
.count('ReportHistory', {})
.then((count) => count.toString())
await db.create('ReportHistory', {
reportId: reportId,
type: reportData.type,
objectId: reportData.objectId,
reportorEpochKey: reportData.reportorEpochKey,
respondentEpochKey: reportData.respondentEpochKey,
reason: reportData.reason,
category: reportData.category,
reportEpoch: reportData.reportEpoch,
})
return reportId
}
async updateObjectStatus(db: DB, reportData: ReportHistory) {
if (reportData.type === ReportType.POST) {
postService.updatePostStatus(
reportData.objectId,
PostStatus.REPORTED,
db
)
} else if (reportData.type === ReportType.COMMENT) {
commentService.updateCommentStatus(
reportData.objectId,
CommentStatus.REPORTED,
db
)
}
}
async fetchReports(
status: ReportStatus,
synchronizer: UnirepSocialSynchronizer,
db: DB
): Promise<ReportHistory[] | null> {
const epoch = await synchronizer.loadCurrentEpoch()
const statusCondition = {
// VOTING reports meet below condition
// 1. reportEpoch = current Epoch - 1
// 2. the result of adjudication is tie, should vote again
// p.s. synchronizer would handle the adjudication result,
// we can assume all reports whose status is VOTING are already handled by synchronizer
[ReportStatus.VOTING]: {
where: {
AND: [
{
adjudicateCount: {
lt: parseInt(REPORT_SETTLE_VOTE_THRESHOLD),
},
},
{ reportEpoch: { lt: epoch } },
{ status: ReportStatus.VOTING },
],
},
},
// WAITING_FOR_TRANSACTION is for client side to claim reputation use
// reports whose report epoch is equal to current epoch are pending reports
// the status should be always VOTING, so the where clause don't search
// current epoch
[ReportStatus.WAITING_FOR_TRANSACTION]: {
where: {
AND: [
{ reportEpoch: { lt: epoch } },
{ status: ReportStatus.WAITING_FOR_TRANSACTION },
],
},
},
}
const condition = statusCondition[status]
if (!condition) throw Errors.INVALID_REPORT_STATUS()
// fetch object(post / comment) and add into report
const reports = await db.findMany('ReportHistory', condition)
for (let i = 0; i < reports.length; i++) {
const report = reports[i]
const tableName =
report.type == ReportType.POST ? 'Post' : 'Comment'
const object = await db.findOne(tableName, {
where: {
[`${tableName.toLocaleLowerCase()}Id`]: report.objectId,
},
})
reports[i].object = object
}
return reports
}
async voteOnReport(
reportId: string,
adjudicateValue: AdjudicateValue,
publicSignals: PublicSignals,
proof: Groth16Proof,
synchronizer: UnirepSocialSynchronizer,
db: DB
) {
const report = await this.fetchSingleReport(reportId, db)
const nullifier = publicSignals[0]
if (!report) throw Errors.REPORT_NOT_EXIST()
if (report.status != ReportStatus.VOTING)
throw Errors.REPORT_VOTING_ENDED()
await ProofHelper.getAndVerifyReportIdentityProof(
publicSignals,
proof,
synchronizer
)
// check if user voted or not
if (this.isVoted(nullifier, report)) throw Errors.USER_ALREADY_VOTED()
const adjudicatorsNullifier = this.upsertAdjudicatorsNullifier(
nullifier,
adjudicateValue,
report
)
// default value is 0, but insert statement doesn't have this field
// if this field is undefined, assume no one has voted yet.
const adjudicateCount = (report.adjudicateCount ?? 0) + 1
// update adjudicatorsNullifier && adjudicateCount
await db.update('ReportHistory', {
where: {
reportId,
},
update: {
adjudicatorsNullifier,
adjudicateCount,
},
})
}
upsertAdjudicatorsNullifier(
nullifier: string,
adjudicateValue: AdjudicateValue,
report: ReportHistory
): Adjudicator[] {
const newAdjudicator = {
nullifier: nullifier,
adjudicateValue: adjudicateValue,
claimed: false,
}
return report.adjudicatorsNullifier
? [...report.adjudicatorsNullifier, newAdjudicator]
: [newAdjudicator]
}
isVoted(nullifier: string, report: ReportHistory): boolean {
// get all adjudicators from report
const adjudicators = report.adjudicatorsNullifier
// if nullifer is included in adjudicators, return true
return adjudicators
? adjudicators.some(
(adjudicator) => adjudicator.nullifier == nullifier
)
: false
}
async fetchSingleReport(
reportId: string,
db: DB
): Promise<ReportHistory | null> {
const report = await db.findOne('ReportHistory', {
where: {
reportId,
},
})
return report
}
fetchReportCategory() {
return [
{
number: ReportCategory.ATTACK,
description:
'對使用者、特定個人、組織或群體發表中傷、歧視、挑釁、羞辱、謾罵、不雅字詞或人身攻擊等言論',
},
{
number: ReportCategory.SPAM,
description:
'張貼商業廣告內容與連結、邀請碼或內含個人代碼的邀請連結等',
},
{
number: ReportCategory.R18,
description:
'張貼色情裸露、性暗示意味濃厚的內容,惟內容具教育性者不在此限',
},
{
number: ReportCategory.VIOLATION,
description: '違反政府法令之情事',
},
{
number: ReportCategory.DUPLICATE,
description: '重複張貼他人已發表過且完全相同的內容',
},
{
number: ReportCategory.MEANINGLESS,
description: '文章內容空泛或明顯無意義內容',
},
{
number: ReportCategory.OTHER,
description: '其他',
},
]
}
async createReputationHistory(
db: DB,
txHash: string,
change: RepChangeType,
repType: ReputationType,
reportId: string,
reportProof: ReportNullifierProof | ReportNonNullifierProof
) {
await db.create('ReputationHistory', {
transactionHash: txHash,
epoch: Number(reportProof.epoch),
epochKey: String(reportProof.currentEpochKey),
score: change,
type: repType,
reportId,
})
}
private isReportCompleted(report: ReportHistory): boolean {
const allAdjudicatorsClaimed = report.adjudicatorsNullifier?.length
? report.adjudicatorsNullifier.every((adj) => adj.claimed)
: false
return (
!!report.reportorClaimedRep &&
!!report.respondentClaimedRep &&
allAdjudicatorsClaimed
)
}
getReputationType(
isPassed: boolean,
repUserType: RepUserType
): ReputationType {
switch (repUserType) {
case RepUserType.ADJUDICATOR:
return ReputationType.ADJUDICATE
case RepUserType.REPORTER:
return isPassed
? ReputationType.REPORT_SUCCESS
: ReputationType.REPORT_FAILURE
case RepUserType.RESPONDENT:
return ReputationType.BE_REPORTED
default:
throw Errors.INVALID_REP_USER_TYPE()
}
}
getClaimHelper(repUserType: RepUserType): ClaimHelpers {
switch (repUserType) {
case RepUserType.REPORTER:
case RepUserType.RESPONDENT:
return ClaimHelpers.ReportNonNullifierVHelper
case RepUserType.ADJUDICATOR:
return ClaimHelpers.ReportNullifierVHelper
default:
throw Errors.INVALID_REP_USER_TYPE()
}
}
checkAdjudicatorNullifier(
report: ReportHistory,
reportProof: ReportNullifierProof | ReportNonNullifierProof
) {
if (
!report.adjudicatorsNullifier?.some(
(adj) =>
!(reportProof instanceof ReportNonNullifierProof) &&
adj.nullifier === reportProof.reportNullifier.toString()
)
) {
throw new Error('Invalid adjudicator nullifier')
}
}
checkAdjudicatorIsClaimed(
report: ReportHistory,
reportProof: ReportNullifierProof | ReportNonNullifierProof
) {
if (
report.adjudicatorsNullifier?.some(
(adj) =>
!(reportProof instanceof ReportNonNullifierProof) &&
adj.nullifier === reportProof.reportNullifier.toString() &&
adj.claimed
)
) {
throw Errors.USER_ALREADY_CLAIMED()
}
}
checkRespondentEpochKey(
report: ReportHistory,
reportProof: ReportNullifierProof | ReportNonNullifierProof
) {
if (
!(reportProof instanceof ReportNonNullifierProof) ||
report.respondentEpochKey !==
reportProof.reportedEpochKey.toString()
) {
throw new Error('Invalid respondent epoch key')
}
}
checkReportorEpochKey(
report: ReportHistory,
reportProof: ReportNullifierProof | ReportNonNullifierProof
) {
if (
!(reportProof instanceof ReportNonNullifierProof) ||
report.reportorEpochKey !== reportProof.reportedEpochKey.toString()
) {
throw new Error('Invalid reportor epoch key')
}
}
checkRespondentIsClaimed(report: ReportHistory) {
if (report.respondentClaimedRep) throw Errors.USER_ALREADY_CLAIMED()
}
checkReporterIsClaimed(report: ReportHistory) {
if (report.reportorClaimedRep) throw Errors.USER_ALREADY_CLAIMED()
}
getClaimMethod(repUserType: RepUserType, isPassed: boolean): ClaimMethods {
switch (repUserType) {
case RepUserType.RESPONDENT:
if (isPassed) {
return ClaimMethods.CLAIM_NEGATIVE_REP
} else {
throw new Error(
'Poster cannot claim reputation for failed reports'
)
}
case RepUserType.REPORTER:
return isPassed
? ClaimMethods.CLAIM_POSITIVE_REP
: ClaimMethods.CLAIM_NEGATIVE_REP
case RepUserType.ADJUDICATOR:
return ClaimMethods.CLAIM_POSITIVE_REP
default:
throw Errors.INVALID_REP_USER_TYPE()
}
}
getClaimChange(repUserType: RepUserType, isPassed: boolean): RepChangeType {
switch (repUserType) {
case RepUserType.REPORTER:
return isPassed
? RepChangeType.REPORTER_REP
: RepChangeType.FAILED_REPORTER_REP
case RepUserType.ADJUDICATOR:
return RepChangeType.ADJUDICATOR_REP
case RepUserType.RESPONDENT:
return RepChangeType.RESPONDENT_REP
default:
throw Errors.INVALID_REP_USER_TYPE()
}
}
checkReportResult(report: ReportHistory): boolean {
if (
!report.adjudicatorsNullifier ||
report.adjudicatorsNullifier.length === 0
) {
return false
}
let agreeVotes = 0
let disagreeVotes = 0
for (const adjudicator of report.adjudicatorsNullifier) {
if (adjudicator.adjudicateValue === AdjudicateValue.AGREE) {
agreeVotes++
} else if (
adjudicator.adjudicateValue === AdjudicateValue.DISAGREE
) {
disagreeVotes++
}
}
const passThreshold = 0.5
return agreeVotes / (agreeVotes + disagreeVotes) > passThreshold
}
async updateReportStatus(
reportId: string,
repUserType: RepUserType,
db: DB,
reportProof: ReportNullifierProof | ReportNonNullifierProof
) {
const report = await this.fetchSingleReport(reportId, db)
if (!report) throw new Error('Report not found')
let updates: Partial<ReportHistory> = {}
switch (repUserType) {
case RepUserType.REPORTER:
updates.reportorClaimedRep = true
break
case RepUserType.RESPONDENT:
updates.respondentClaimedRep = true
break
case RepUserType.ADJUDICATOR:
if (!report.adjudicatorsNullifier) {
throw new Error('No adjudicators found for this report')
}
updates.adjudicatorsNullifier =
report.adjudicatorsNullifier.map((adj) => {
if (
!(reportProof instanceof ReportNonNullifierProof) &&
adj.nullifier ===
reportProof.reportNullifier.toString()
) {
return { ...adj, claimed: true }
}
return adj
})
break
default:
throw Errors.INVALID_REP_USER_TYPE()
}
const updatedReport = { ...report, ...updates }
if (this.isReportCompleted(updatedReport)) {
updates.status = ReportStatus.COMPLETED
}
await db.update('ReportHistory', {
where: { reportId },
update: updates,
})
return await this.fetchSingleReport(reportId, db)
}
async claim(
claimMethod: ClaimMethods,
claimChange: RepChangeType,
identifier: string,
publicSignals: any,
proof: any
): Promise<string> {
let txHash: string | undefined
try {
txHash = await TransactionManager.callContract(claimMethod, [
publicSignals,
proof,
identifier,
claimChange,
])
if (!txHash) {
throw new Error(
'Transaction hash is undefined after contract call'
)
}
return txHash
} catch (error: any) {
console.error('Error in claiming reputation:', error)
throw new Error(`Reputation claim failed: ${error.message}`)
}
}
async validateClaimRequest(
report: ReportHistory,
repUserType: RepUserType,
reportProof: ReportNullifierProof | ReportNonNullifierProof
) {
if (repUserType === RepUserType.ADJUDICATOR) {
this.checkAdjudicatorNullifier(report, reportProof)
this.checkAdjudicatorIsClaimed(report, reportProof)
} else if (repUserType === RepUserType.RESPONDENT) {
this.checkRespondentEpochKey(report, reportProof)
this.checkRespondentIsClaimed(report)
} else if (repUserType === RepUserType.REPORTER) {
this.checkReportorEpochKey(report, reportProof)
this.checkReporterIsClaimed(report)
} else {
throw Errors.INVALID_REP_USER_TYPE()
}
}
async getEpochAndEpochKey(
claimSignals: any,
claimProof: any,
repUserType: RepUserType
) {
let currentEpoch: any, currentEpochKey: any, reportedEpochKey: any
if (repUserType === RepUserType.ADJUDICATOR) {
const reportNullifierProof = new ReportNullifierProof(
claimSignals,
claimProof
)
currentEpoch = reportNullifierProof.epoch
currentEpochKey = reportNullifierProof.currentEpochKey
} else {
const reportNonNullifierProof = new ReportNonNullifierProof(
claimSignals,
claimProof
)
currentEpoch = reportNonNullifierProof.epoch
currentEpochKey = reportNonNullifierProof.currentEpochKey
reportedEpochKey = reportNonNullifierProof.reportedEpochKey
}
return { currentEpoch, currentEpochKey, reportedEpochKey }
}
async getReportProof(
claimSignals: any,
claimProof: any,
repUserType: RepUserType,
synchronizer: UnirepSocialSynchronizer
): Promise<ReportNullifierProof | ReportNonNullifierProof> {
if (repUserType === RepUserType.ADJUDICATOR) {
return new ReportNullifierProof(
claimSignals,
claimProof,
synchronizer.prover
)
} else {
return new ReportNonNullifierProof(
claimSignals,
claimProof,
synchronizer.prover
)
}
}
}
export const reportService = new ReportService()