From 896ed595eb76563a894556bd93eb9ec3276356dd Mon Sep 17 00:00:00 2001 From: motechFR Date: Thu, 31 Oct 2024 17:37:12 +0300 Subject: [PATCH] Add the code for claims and proof generation for the merkle trees --- .../src/claims/__tests__/verifyClaim.spec.ts | 51 ++++++++++++++----- .../src/claims/generateWeeklyClaims.ts | 50 ++++++++++++++++-- .../src/claims/{root.ts => merkleTree.ts} | 15 ++++-- 3 files changed, 94 insertions(+), 22 deletions(-) rename packages/scoutgame/src/claims/{root.ts => merkleTree.ts} (55%) diff --git a/packages/scoutgame/src/claims/__tests__/verifyClaim.spec.ts b/packages/scoutgame/src/claims/__tests__/verifyClaim.spec.ts index a9ea88b6b2..6a750593e4 100644 --- a/packages/scoutgame/src/claims/__tests__/verifyClaim.spec.ts +++ b/packages/scoutgame/src/claims/__tests__/verifyClaim.spec.ts @@ -1,43 +1,66 @@ -import { generateTree, getProofs, verifyClaim, type ProvableClaim } from '../root'; +import { generateMerkleTree, getMerkleProofs, verifyMerkleClaim, type ProvableClaim } from '../merkleTree'; const claimsInput: ProvableClaim[] = [ { address: '0x36446eF671954753801f9d73C415a80C0e550b32', - amount: '100' + amount: 100 }, { address: '0xC82ee528AC8BFd7087e0DE6548955601dFcac99d', - amount: '200' + amount: 200 }, { address: '0xB20C9b7e6b9cbcDed9819F88D68938D0B149887f', - amount: '300' + amount: 300 }, { address: '0x36446eF671954753801f9d73C415a80C0e550b32', - amount: '400' + amount: 400 }, { address: '0xD02953857250D32EC72064d9E2320B43296E52C0', - amount: '500' + amount: 500 } ]; -describe('verifyClaim', () => { +describe('verifyMerkleClaim', () => { it('should return true if the claim is valid', () => { - const tree = generateTree(claimsInput); + const { tree } = generateMerkleTree(claimsInput); const claim = claimsInput[0]; - const proofs = getProofs(tree, claim); - expect(verifyClaim(tree, claim, proofs)).toBe(true); + const proofs = getMerkleProofs(tree, claim); + expect(verifyMerkleClaim(tree, claim, proofs)).toBe(true); }); it('should return false if the claim is invalid', () => { - const tree = generateTree(claimsInput); + const { tree } = generateMerkleTree(claimsInput); const claim: ProvableClaim = { address: '0x36446eF671954753801f9d73C415a80C0e550b32', - amount: '200' + amount: 200 }; - const proof = getProofs(tree, claim); - expect(verifyClaim(tree, claim, proof)).toBe(false); + const proof = getMerkleProofs(tree, claim); + expect(verifyMerkleClaim(tree, claim, proof)).toBe(false); + }); + + it('should sort inputs so that it is not reliant on ordering of the claims', () => { + function shuffleArray(array: T[]): T[] { + const newArray = [...array]; // Create a copy of the array to avoid mutating the original + for (let i = newArray.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; // Swap elements + } + return newArray; + } + + const shuffledOne = shuffleArray(claimsInput); + + const shuffledTwo = shuffleArray(claimsInput); + + // Make sure sorting worked + expect(JSON.stringify(shuffledOne)).not.toEqual(JSON.stringify(shuffledTwo)); + + const { rootHash: rootHashOne } = generateMerkleTree(shuffledOne); + const { rootHash: rootHashTwo } = generateMerkleTree(shuffledTwo); + + expect(rootHashOne).toEqual(rootHashTwo); }); }); diff --git a/packages/scoutgame/src/claims/generateWeeklyClaims.ts b/packages/scoutgame/src/claims/generateWeeklyClaims.ts index 3fd6f8aabc..e8948283fb 100644 --- a/packages/scoutgame/src/claims/generateWeeklyClaims.ts +++ b/packages/scoutgame/src/claims/generateWeeklyClaims.ts @@ -1,3 +1,4 @@ +import type { WeeklyClaims } from '@charmverse/core/prisma-client'; import { prisma } from '@charmverse/core/prisma-client'; import type { Address } from 'viem'; @@ -5,11 +6,20 @@ import { currentSeason } from '../dates'; import { dividePointsBetweenBuilderAndScouts } from '../points/dividePointsBetweenBuilderAndScouts'; import { getWeeklyPointsPoolAndBuilders } from '../points/getWeeklyPointsPoolAndBuilders'; -import type { ProvableClaim } from './root'; +import { generateMerkleTree, type ProvableClaim } from './merkleTree'; -export async function generateWeeklyClaims({ week }: { week: string }): Promise { - const { normalisationFactor, topWeeklyBuilders, totalPoints, weeklyAllocatedPoints } = - await getWeeklyPointsPoolAndBuilders({ week }); +type ClaimsBody = { + leaves: ProvableClaim[]; +}; + +type WeeklyClaimsTyped = Omit & { + claims: ClaimsBody; +}; + +export async function calculateWeeklyClaims({ week }: { week: string }): Promise { + const { normalisationFactor, topWeeklyBuilders, weeklyAllocatedPoints } = await getWeeklyPointsPoolAndBuilders({ + week + }); const allClaims = await Promise.all( topWeeklyBuilders.map(async (builder) => { @@ -84,3 +94,35 @@ export async function generateWeeklyClaims({ week }: { week: string }): Promise< return claimsByAddress; } + +export async function generateWeeklyClaims({ week }: { week: string }): Promise { + const existingClaim = await prisma.weeklyClaims.findUnique({ + where: { + week + } + }); + + if (existingClaim) { + throw new Error(`Claims for week ${week} already exist`); + } + + const claims = await calculateWeeklyClaims({ week }); + + const { rootHash } = generateMerkleTree(claims); + + const claimsBody: ClaimsBody = { + leaves: claims + }; + + const weeklyClaim = await prisma.weeklyClaims.create({ + data: { + week, + merkleTreeRoot: rootHash, + season: currentSeason, + totalClaimable: claims.reduce((acc, claim) => acc + claim.amount, 0), + claims: claimsBody + } + }); + + return weeklyClaim as WeeklyClaimsTyped; +} diff --git a/packages/scoutgame/src/claims/root.ts b/packages/scoutgame/src/claims/merkleTree.ts similarity index 55% rename from packages/scoutgame/src/claims/root.ts rename to packages/scoutgame/src/claims/merkleTree.ts index 124b5f9f3f..f065954a08 100644 --- a/packages/scoutgame/src/claims/root.ts +++ b/packages/scoutgame/src/claims/merkleTree.ts @@ -16,15 +16,22 @@ function claimToString(claim: ProvableClaim): string { return `${claim.address}:${claim.amount}`; } -export function generateTree(claims: ProvableClaim[]): MerkleTree { - return new MerkleTree(claims.map(claimToString), SHA256); +export function generateMerkleTree(claims: ProvableClaim[]): { tree: MerkleTree; rootHash: string } { + const tree = new MerkleTree(claims.map(claimToString), SHA256, { + sort: true + }); + + return { + tree, + rootHash: tree.getRoot().toString('hex') + }; } -export function getProofs(tree: MerkleTree, claim: ProvableClaim): any { +export function getMerkleProofs(tree: MerkleTree, claim: ProvableClaim): any { return tree.getProof(claimToString(claim)); } -export function verifyClaim(tree: MerkleTree, claim: ProvableClaim, proof: string[]): boolean { +export function verifyMerkleClaim(tree: MerkleTree, claim: ProvableClaim, proof: string[]): boolean { const root = tree.getRoot().toString('hex'); return tree.verify(proof, claimToString(claim), root); }