Skip to content

Commit

Permalink
feat(reporting): claim reputation in background (UST-138)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
UranusLin committed Sep 1, 2024
1 parent ba59b3c commit c2cded8
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 92 deletions.
1 change: 1 addition & 0 deletions packages/frontend/src/constants/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum QueryKeys {
CommentHistory = 'comment_history',
VoteHistory = 'vote_history',
ReportHistory = 'ReportHistory',
ReportsWaitingForTransaction = 'ReportsWaitingForTransaction',
}

export enum MutationKeys {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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,
Expand Down
54 changes: 18 additions & 36 deletions packages/frontend/src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -336,60 +338,40 @@ 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,
}),
),
})

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()
}
3 changes: 0 additions & 3 deletions packages/relay/src/services/ReportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ import {
CommentNotExistError,
CommentReportedError,
InvalidCommentIdError,
InvalidParametersError,
InvalidPostIdError,
InvalidReportNullifierError,
InvalidReportStatusError,
InvalidRepUserTypeError,
InvalidReputationProofError,
PostNotExistError,
PostReportedError,
ReportCategory,
Expand Down

0 comments on commit c2cded8

Please sign in to comment.