From c2cded841dbf57ca6e2c302e135d36032eb1a3f1 Mon Sep 17 00:00:00 2001 From: MorrisLin Date: Mon, 2 Sep 2024 03:22:57 +0800 Subject: [PATCH] feat(reporting): claim reputation in background (UST-138) Automatically claim reputation for reporters, posters, and voters when a report is resolved. Claims reputation by generating a zero-knowledge proof and sending it to the relay server. Includes: - Fetch reports waiting for transaction from the relay server - Generate zero-knowledge proofs for reporters, posters, and voters - Submit proofs to the relay server to claim reputation - Refetch reports and invalidate queries after claiming reputation --- packages/frontend/src/constants/queryKeys.ts | 1 + .../useBackgroundReputationClaim.ts | 210 +++++++++++++----- packages/frontend/src/utils/api.ts | 54 ++--- packages/relay/src/services/ReportService.ts | 3 - 4 files changed, 176 insertions(+), 92 deletions(-) diff --git a/packages/frontend/src/constants/queryKeys.ts b/packages/frontend/src/constants/queryKeys.ts index 87637cd2c..7ac9d9205 100644 --- a/packages/frontend/src/constants/queryKeys.ts +++ b/packages/frontend/src/constants/queryKeys.ts @@ -16,6 +16,7 @@ export enum QueryKeys { CommentHistory = 'comment_history', VoteHistory = 'vote_history', ReportHistory = 'ReportHistory', + ReportsWaitingForTransaction = 'ReportsWaitingForTransaction', } export enum MutationKeys { diff --git a/packages/frontend/src/features/post/hooks/useBackgroundReputationClaim/useBackgroundReputationClaim.ts b/packages/frontend/src/features/post/hooks/useBackgroundReputationClaim/useBackgroundReputationClaim.ts index dd45f8732..39aeeac49 100644 --- a/packages/frontend/src/features/post/hooks/useBackgroundReputationClaim/useBackgroundReputationClaim.ts +++ b/packages/frontend/src/features/post/hooks/useBackgroundReputationClaim/useBackgroundReputationClaim.ts @@ -1,59 +1,119 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useUserState } from '@/features/core' import { MutationKeys, QueryKeys } from '@/constants/queryKeys' import { - relayClaimPositiveReputation, - relayClaimNegativeReputation, + fetchReportsWaitingForTransaction, + relayClaimReputation, } from '@/utils/api' import { RepUserType, ReputationType } from '@/types/Report' +import { useEffect, useCallback } from 'react' +import { + flattenProof, + genProofAndVerify, + genReportNonNullifierCircuitInput, + genReportNullifierCircuitInput, +} from '@unirep-app/contracts/test/utils' +import { genNullifier } from '@unirep-app/circuits/test/utils' +import { UnirepSocialCircuit } from '@unirep-app/circuits/dist/src/types' +import { ReportHistory } from '@/features/reporting/utils/types' +import { UserState } from '@unirep/core' + +type ClaimReputationParams = { + report: ReportHistory + repUserType: RepUserType +} + +const REFETCH_INTERVAL = 60000 export function useBackgroundReputationClaim() { const queryClient = useQueryClient() const { getGuaranteedUserState } = useUserState() - const claimReputation = useMutation({ - mutationKey: [MutationKeys.ClaimReputation], - mutationFn: async ({ - reportId, - repUserType, - isPositive, - nullifier, - }: { - reportId: string - repUserType: RepUserType - isPositive: boolean - nullifier?: string - }) => { - const userState = await getGuaranteedUserState() - const epochKeyProof = await userState.genEpochKeyLiteProof() + const { data: reportsWaitingForTransaction, refetch: refetchReports } = + useQuery({ + queryKey: [QueryKeys.ReportsWaitingForTransaction], + queryFn: fetchReportsWaitingForTransaction, + refetchInterval: REFETCH_INTERVAL, + }) + + const generateProof = useCallback( + async ( + userState: UserState, + report: ReportHistory, + repUserType: RepUserType, + ) => { + const epochKeyProof = await userState.genEpochKeyProof() + const currentEpoch = epochKeyProof.epoch + const currentNonce = epochKeyProof.nonce + const attesterId = epochKeyProof.attesterId + const chainId = userState.chainId + const identitySecret = userState.id.secret - let result - if (isPositive) { - result = await relayClaimPositiveReputation( - epochKeyProof, - reportId, - repUserType, - nullifier, + if (repUserType === RepUserType.VOTER) { + const reportNullifier = genNullifier( + userState.id, + Number(report.reportId), + ) + const reportNullifierCircuitInputs = + genReportNullifierCircuitInput({ + reportNullifier, + identitySecret, + reportId: 1, + currentEpoch, + currentNonce, + attesterId, + chainId, + }) + return genProofAndVerify( + UnirepSocialCircuit.reportNullifierProof, + reportNullifierCircuitInputs, ) } else { - result = await relayClaimNegativeReputation( - epochKeyProof, - reportId, - repUserType, + const reporterEpochKey = + repUserType === RepUserType.REPORTER + ? report.reportorEpochKey + : report.respondentEpochKey + const reportNonNullifierCircuitInput = + genReportNonNullifierCircuitInput({ + reportedEpochKey: reporterEpochKey, + identitySecret, + reportedEpoch: 0, + currentEpoch, + currentNonce, + chainId, + attesterId, + }) + return genProofAndVerify( + UnirepSocialCircuit.reportNonNullifierProof, + reportNonNullifierCircuitInput, ) } + }, + [], + ) + + const claimReputation = useMutation({ + mutationKey: [MutationKeys.ClaimReputation], + mutationFn: async ({ report, repUserType }: ClaimReputationParams) => { + const userState = await getGuaranteedUserState() + const { publicSignals, proof } = await generateProof( + userState, + report, + repUserType, + ) + const result = await relayClaimReputation( + report.reportId, + repUserType, + publicSignals, + flattenProof(proof), + ) await userState.waitForSync() return { - txHash: result.txHash, - reportId, - epoch: epochKeyProof.epoch, - epochKey: epochKeyProof.epochKey.toString(), - type: isPositive - ? ReputationType.REPORT_SUCCESS - : ReputationType.REPORT_FAILURE, - score: result.score, // Assuming the API returns the score + ...result, + epoch: userState.sync.calcCurrentEpoch(), + epochKey: userState.getEpochKeys(), } }, onSuccess: (data) => { @@ -63,26 +123,70 @@ export function useBackgroundReputationClaim() { queryClient.invalidateQueries({ queryKey: [QueryKeys.ReportHistory, data.reportId], }) + refetchReports() }, }) - const claimReputationInBackground = async ( - reportId: string, - repUserType: RepUserType, - isPositive: boolean, - nullifier?: string, - ) => { - try { - await claimReputation.mutateAsync({ - reportId, - repUserType, - isPositive, - nullifier, - }) - } catch (error) { - console.error('Failed to claim reputation in background:', error) + const claimReputationInBackground = useCallback( + async (report: ReportHistory, repUserType: RepUserType) => { + try { + await claimReputation.mutateAsync({ report, repUserType }) + } catch (error) { + console.error( + 'Failed to claim reputation in background:', + error, + ) + } + }, + [claimReputation], + ) + + useEffect(() => { + const processReports = async () => { + if (!reportsWaitingForTransaction) return + + const userState = await getGuaranteedUserState() + + for (const report of reportsWaitingForTransaction) { + const epoch = report.reportEpoch + const epochKeyLiteProof = await userState.genEpochKeyLiteProof({ + epoch: epoch, + }) + const currentEpochKey = epochKeyLiteProof.epochKey.toString() + if ( + report.reportorEpochKey === currentEpochKey && + !report.reportorClaimedRep + ) { + await claimReputationInBackground( + report, + RepUserType.REPORTER, + ) + } else if ( + report.respondentEpochKey === currentEpochKey && + !report.respondentClaimedRep + ) { + await claimReputationInBackground( + report, + RepUserType.POSTER, + ) + } else if ( + report.adjudicatorsNullifier?.some( + (adj) => + adj.nullifier === currentEpochKey && !adj.claimed, + ) ?? + false + ) { + await claimReputationInBackground(report, RepUserType.VOTER) + } + } } - } + + processReports() + }, [ + claimReputationInBackground, + getGuaranteedUserState, + reportsWaitingForTransaction, + ]) return { claimReputationInBackground, diff --git a/packages/frontend/src/utils/api.ts b/packages/frontend/src/utils/api.ts index 6f35d0144..be8e934dc 100644 --- a/packages/frontend/src/utils/api.ts +++ b/packages/frontend/src/utils/api.ts @@ -1,6 +1,6 @@ import { RelayRawComment } from '@/types/Comments' import { RelayRawPost } from '@/types/Post' -import { ReportCategory, ReportType } from '@/types/Report' +import { ReportCategory, ReportType, RepUserType } from '@/types/Report' import { VoteAction } from '@/types/Vote' import { EpochKeyLiteProof, @@ -29,6 +29,8 @@ import { RelayUserStateTransitionResponse, SortKeys, } from '../types/api' +import { ReportHistory } from '@/features/reporting/utils/types' +import { ReportStatus } from '@unirep-app/relay/build/src/types' export async function fetchReputationHistory( fromEpoch: number, @@ -336,24 +338,23 @@ export async function relayReport({ return data } -export async function relayClaimPositiveReputation( - proof: EpochKeyLiteProof, +export async function relayClaimReputation( reportId: string, - repUserType: string, - nullifier?: string, + repUserType: RepUserType, + publicSignals: string[], + proof: string[], ) { - const response = await fetch(`${SERVER}/api/claim-positive-reputation`, { + const response = await fetch(`${SERVER}/api/reputation/claim`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify( stringifyBigInts({ - publicSignals: proof.publicSignals, - proof: proof.proof, reportId, repUserType, - nullifier, + claimSignals: publicSignals, + claimProof: proof, }), ), }) @@ -361,35 +362,16 @@ export async function relayClaimPositiveReputation( const data = await response.json() if (!response.ok) { - throw Error(`Claim positive reputation failed: ${data.error}`) + throw Error(`Claim reputation failed: ${data.error}`) } return data } -export async function relayClaimNegativeReputation( - proof: EpochKeyLiteProof, - reportId: string, - repUserType: string, -) { - const response = await fetch(`${SERVER}/api/claim-negative-reputation`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify( - stringifyBigInts({ - publicSignals: proof.publicSignals, - proof: proof.proof, - reportId, - repUserType, - }), - ), - }) - - const data = await response.json() - - if (!response.ok) { - throw Error(`Claim negative reputation failed: ${data.error}`) - } - return data +export async function fetchReportsWaitingForTransaction(): Promise< + ReportHistory[] +> { + const response = await fetch( + `${SERVER}/api/report?status=${ReportStatus.WAITING_FOR_TRANSACTION}`, + ) + return response.json() } diff --git a/packages/relay/src/services/ReportService.ts b/packages/relay/src/services/ReportService.ts index 683f24fbf..2ca26f78b 100644 --- a/packages/relay/src/services/ReportService.ts +++ b/packages/relay/src/services/ReportService.ts @@ -8,12 +8,9 @@ import { CommentNotExistError, CommentReportedError, InvalidCommentIdError, - InvalidParametersError, InvalidPostIdError, - InvalidReportNullifierError, InvalidReportStatusError, InvalidRepUserTypeError, - InvalidReputationProofError, PostNotExistError, PostReportedError, ReportCategory,