Skip to content

Commit

Permalink
Relocate protocol eas and merkle proofs to core
Browse files Browse the repository at this point in the history
  • Loading branch information
motechFR committed Nov 7, 2024
1 parent 9b42ef0 commit 2e017e2
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 1 deletion.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"./http": {
"import": "./dist/esm/http.js",
"require": "./dist/cjs/http.js"
},
"./protocol": {
"import": "./dist/esm/protocol.js",
"require": "./dist/cjs/protocol.js"
}
},
"files": [
Expand Down Expand Up @@ -142,19 +146,21 @@
},
"dependencies": {
"@datadog/browser-logs": "^4.42.2",
"@ethereum-attestation-service/eas-sdk": "^0.29.1",
"@prisma/client": "^5.7.1",
"async-sema": "^3.1.1",
"fetch-retry": "^5.0.6",
"lodash": "^4.17.21",
"loglevel": "^1.8.1",
"luxon": "^3.3.0",
"merkletreejs": "^0.4.0",
"nanoid": "^3.3.6",
"nanoid-dictionary": "^3.0.0",
"prisma": "^5.7.1",
"undici": "^6.19.5",
"utf-8-validate": "^5.0.10",
"uuid": "^9.0.0",
"viem": "^1.18.3"
"viem": "^2.21.42"
},
"license": "GPLv3"
}
8 changes: 8 additions & 0 deletions src/lib/protocol/easSchemas/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getSchemaUID } from '@ethereum-attestation-service/eas-sdk';

export const NULL_EAS_REF_UID = '0x0000000000000000000000000000000000000000000000000000000000000000';

export const NULL_EVM_ADDRESS = '0x0000000000000000000000000000000000000000';

// Obtained from https://github.com/ethereum-attestation-service/eas-contracts/blob/558250dae4cb434859b1ac3b6d32833c6448be21/deploy/scripts/000004-name-initial-schemas.ts#L10C1-L11C1
export const NAME_SCHEMA_UID = getSchemaUID('bytes32 schemaId,string name', NULL_EVM_ADDRESS, true);
56 changes: 56 additions & 0 deletions src/lib/protocol/easSchemas/githubContributionReceiptSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk';

export const githubContributionReceiptEASSchema =
'bytes32 userRefUID,string description,string url,string metadataUrl,uint256 value,string type';

export const githubContributionReceiptSchemaName = 'Github Contribution Receipt';

export type GithubContributionReceiptAttestation = {
userRefUID: string;
description: string;
url: string;
metadataUrl: string;
value: number;
type: string;
};

const encoder = new SchemaEncoder(githubContributionReceiptEASSchema);

export function encodeGithubContributionReceiptAttestation(
attestation: GithubContributionReceiptAttestation
): `0x${string}` {
const encodedData = encoder.encodeData([
{ name: 'userRefUID', type: 'string', value: attestation.userRefUID },
{ name: 'description', type: 'string', value: attestation.description },
{
name: 'url',
type: 'string',
value: attestation.url
},
{
name: 'metadataUrl',
type: 'string',
value: attestation.metadataUrl
},
{ name: 'value', type: 'uint256', value: attestation.value },
{ name: 'type', type: 'string', value: attestation.type }
]);

return encodedData as `0x${string}`;
}

export function decodeGithubContributionReceiptAttestation(rawData: string): GithubContributionReceiptAttestation {
const parsed = encoder.decodeData(rawData);
const values = parsed.reduce((acc, item) => {
const key = item.name as keyof GithubContributionReceiptAttestation;

if (key === 'value') {
acc[key] = parseInt(item.value.value as string);
} else {
acc[key] = item.value.value as string;
}
return acc;
}, {} as GithubContributionReceiptAttestation);

return values as GithubContributionReceiptAttestation;
}
3 changes: 3 additions & 0 deletions src/lib/protocol/easSchemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './constants';
export * from './githubContributionReceiptSchema';
export * from './scoutGameUserProfileSchema';
34 changes: 34 additions & 0 deletions src/lib/protocol/easSchemas/scoutGameUserProfileSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk';

export const scoutGameUserProfileEASSchema = 'string id,string metadataUrl';

export const scoutGameUserProfileSchemaName = 'Scout Game User Profile';

export type ScoutGameUserProfileAttestation = {
id: string;
metadataUrl: string;
};

const encoder = new SchemaEncoder(scoutGameUserProfileEASSchema);

export function encodeScoutGameUserProfileAttestation(attestation: ScoutGameUserProfileAttestation): `0x${string}` {
const encodedData = encoder.encodeData([
{ name: 'id', type: 'string', value: attestation.id },
{ name: 'metadataUrl', type: 'string', value: attestation.metadataUrl }
]);

return encodedData as `0x${string}`;
}

export function decodeScoutGameUserProfileAttestation(rawData: string): ScoutGameUserProfileAttestation {
const parsed = encoder.decodeData(rawData);
const values = parsed.reduce((acc, item) => {
const key = item.name as keyof ScoutGameUserProfileAttestation;

acc[key] = item.value.value as string;

return acc;
}, {} as ScoutGameUserProfileAttestation);

return values as ScoutGameUserProfileAttestation;
}
70 changes: 70 additions & 0 deletions src/lib/protocol/proofs/__tests__/verifyClaim.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { generateMerkleTree, getMerkleProofs, verifyMerkleClaim, type ProvableClaim } from '../merkleTree';

const claimsInput: ProvableClaim[] = [
{
// Key here so we can copy to other tests: 57b7b9b29419b66ac8156f844a7b0eb18d94f729699b3f15a3d8817d3f5980a3
address: '0x3F2A655d4e39E6c4470703e1063e9a843586886A',
amount: 100
},
{
// Key here so we can copy to other tests: aa03d22263ff3e4df4105a20d08f62873f5e100974862fdc1f99083ba11e6adc
address: '0x2Fe1B8C9C8722f0D3e5B9a9D4115559bB8f04931',
amount: 200
},
{
// Key here so we can copy to other tests: c674865dde0163f480f818a78fc4d316c64d60b05666600734df8e8f37147f64
address: '0x03F8B139fF6dbbb7475bAA5A71c16fcDD9495cc4',
amount: 300
},
{
address: '0x36446eF671954753801f9d73C415a80C0e550b32',
amount: 400
},
{
address: '0xD02953857250D32EC72064d9E2320B43296E52C0',
amount: 500
}
];

describe('verifyMerkleClaim', () => {
it('should return true if the claim is valid', () => {
const { tree } = generateMerkleTree(claimsInput);
const claim = claimsInput[0];
const proofs = getMerkleProofs(tree, claim);

expect(verifyMerkleClaim(tree, claim, proofs)).toBe(true);
});

it('should return false if the claim is invalid', () => {
const { tree } = generateMerkleTree(claimsInput);
const claim: ProvableClaim = {
address: '0x36446eF671954753801f9d73C415a80C0e550b32',
amount: 200
};
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<T>(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);
});
});
37 changes: 37 additions & 0 deletions src/lib/protocol/proofs/merkleTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { MerkleTree } from 'merkletreejs';
import type { Address } from 'viem';
import { keccak256, encodePacked } from 'viem/utils';

export type ProvableClaim = {
address: Address;
amount: number;
};

function hashLeaf(claim: ProvableClaim): string {
// Mimic Solidity's keccak256(abi.encodePacked(address, amount))
const packedData = encodePacked(['address', 'uint256'], [claim.address, BigInt(claim.amount)]);
return keccak256(packedData);
}

export function generateMerkleTree(claims: ProvableClaim[]): { tree: MerkleTree; rootHash: string } {
const inputs = claims.map(hashLeaf);

const tree = new MerkleTree(inputs, keccak256, {
sort: true
// concatenator: ([left, right]) => Buffer.from(hashLeaf(Buffer.concat([left, right]).toString()))
});

return {
tree,
rootHash: tree.getRoot().toString('hex')
};
}

export function getMerkleProofs(tree: MerkleTree, claim: ProvableClaim): string[] {
return tree.getHexProof(hashLeaf(claim));
}

export function verifyMerkleClaim(tree: MerkleTree, claim: ProvableClaim, proof: string[]): boolean {
const root = tree.getRoot().toString('hex');
return tree.verify(proof, hashLeaf(claim), root);
}
2 changes: 2 additions & 0 deletions src/protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/protocol/proofs/merkleTree';
export * from './lib/protocol/easSchemas/index';

0 comments on commit 2e017e2

Please sign in to comment.