Skip to content

Require user signatures #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,978 changes: 784 additions & 1,194 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@types/node": "^16.18.119",
"bcrypt": "^5.1.1",
"concurrently": "^9.0.1",
"deterministic-object-hash": "^2.0.2",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"express-validator": "^7.2.0",
Expand All @@ -42,7 +43,8 @@
"reflect-metadata": "^0.1.14",
"tsconfig-paths": "^4.2.0",
"typeorm": "^0.3.20",
"typescript": "^4.9.5",
"typescript": "^5.6.3",
"viem": "^2.21.47",
"winston": "^3.16.0"
},
"scripts": {
Expand Down
74 changes: 60 additions & 14 deletions src/controllers/evaluationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { Request, Response } from 'express';
import evaluationService, {
type CreateEvaluationParams,
} from '@/service/EvaluationService';
import { addressFrom, catchError, validateRequest } from '@/utils';
import {
addressFrom,
catchError,
isPoolManager,
validateRequest,
} from '@/utils';
import { createLogger } from '@/logger';
import applicationService from '@/service/ApplicationService';
import poolService from '@/service/PoolService';
Expand All @@ -18,9 +23,14 @@ import {
} from '@/ext/indexer';
import { type Evaluation, EVALUATOR_TYPE } from '@/entity/Evaluation';
import { IsNullError, NotFoundError } from '@/errors';
import { type Hex } from 'viem';

const logger = createLogger();

interface EvaluationBody extends CreateEvaluationParams {
signature: Hex;
}

export const evaluateApplication = async (
req: Request,
res: Response
Expand All @@ -34,12 +44,37 @@ export const evaluateApplication = async (
evaluator,
summaryInput,
chainId,
}: CreateEvaluationParams = req.body;
signature,
}: EvaluationBody = req.body;

logger.info(
`Received evaluation request for alloApplicationId: ${alloApplicationId} in poolId: ${alloPoolId}`
);

const createEvaluationParams: CreateEvaluationParams = {
chainId,
alloPoolId,
alloApplicationId,
cid,
evaluator,
summaryInput,
};

const isAllowed = await isPoolManager<CreateEvaluationParams>(
createEvaluationParams,
signature,
chainId,
alloPoolId
);

if (!isAllowed) {
logger.warn(
`User with address: ${evaluator} is not allowed to evaluate application`
);
res.status(403).json({ message: 'Unauthorized' });
return;
}

const [errorFetching, application] = await catchError(
applicationService.getApplicationByChainIdPoolIdApplicationId(
alloPoolId,
Expand Down Expand Up @@ -67,14 +102,7 @@ export const evaluateApplication = async (
}

const [evaluationError, evaluationResponse] = await catchError(
createEvaluation({
chainId,
alloPoolId,
alloApplicationId,
cid,
evaluator,
summaryInput,
})
createEvaluation(createEvaluationParams)
);

if (evaluationError !== undefined || evaluationResponse === null) {
Expand Down Expand Up @@ -123,21 +151,39 @@ export interface CreateLLMEvaluationParams {
applicationMetadata?: ApplicationMetadata;
questions?: PromptEvaluationQuestions;
}

interface PoolIdChainIdApplicationId {
alloPoolId: string;
chainId: number;
alloApplicationId: string;
}

interface PoolIdChainIdApplicationIdBody extends PoolIdChainIdApplicationId {
signature: Hex;
}

export const triggerLLMEvaluation = async (
req: Request,
res: Response
): Promise<void> => {
validateRequest(req, res);

const { alloPoolId, chainId, alloApplicationId } =
req.body as PoolIdChainIdApplicationId;
const { alloPoolId, chainId, alloApplicationId, signature } =
req.body as PoolIdChainIdApplicationIdBody;

const isAllowed = await isPoolManager<PoolIdChainIdApplicationId>(
{ alloPoolId, chainId, alloApplicationId },
signature,
chainId,
alloPoolId
);

if (!isAllowed) {
logger.warn(
`User with address: ${signature} is not allowed to evaluate application`
);
res.status(403).json({ message: 'Unauthorized' });
return;
}

const questions = await evaluationService.getQuestionsByChainAndAlloPoolId(
chainId,
Expand Down Expand Up @@ -197,7 +243,7 @@ export const createLLMEvaluations = async (
: params.questions;

if (evaluationQuestions === null || evaluationQuestions.length === 0) {
logger.error('Failed to get evaluation questions');
logger.error('createLLMEvaluations:Failed to get evaluation questions');
throw new Error('Failed to get evaluation questions');
}

Expand Down
56 changes: 55 additions & 1 deletion src/ext/indexer/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import type {
RoundWithApplications,
ApplicationRoundQueryResponse,
ApplicationWithRound,
ManagerRolesResponse,
} from './types';
import request from 'graphql-request';
import { getRoundWithApplications, getApplicationWithRound } from './queries';
import {
getRoundWithApplications,
getApplicationWithRound,
getRoundManager,
} from './queries';
import type { Logger } from 'winston';
import { IsNullError } from '@/errors';
import { env } from '@/env';
Expand Down Expand Up @@ -41,6 +46,55 @@ class IndexerClient {
return IndexerClient.instance;
}

async getRoundManager({
chainId,
alloPoolId,
}: {
chainId: number;
alloPoolId: string;
}): Promise<string[]> {
this.logger.debug(
`Requesting round manager for poolId: ${alloPoolId}, chainId: ${chainId}`
);

const requestVariables = {
chainId,
alloPoolId,
};

try {
const response: ManagerRolesResponse = await request(
this.indexerEndpoint,
getRoundManager,
requestVariables
);

if (response.rounds.length === 0) {
this.logger.warn(
`No round found for poolId: ${alloPoolId} on chainId: ${chainId}`
);
return [];
}

const round = response.rounds[0];

if (round.roles.length === 0) {
this.logger.warn(
`No manager found for poolId: ${alloPoolId} on chainId: ${chainId}`
);
return [];
}

this.logger.info(`Successfully fetched round manager`);
return round.roles.map(role => role.address);
} catch (error) {
this.logger.error(`Failed to fetch round manager: ${error.message}`, {
error,
});
throw error;
}
}

async getRoundWithApplications({
chainId,
roundId,
Expand Down
12 changes: 12 additions & 0 deletions src/ext/indexer/queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { gql } from 'graphql-request';

export const getRoundManager = gql`
query RoundManager($chainId: Int!, $alloPoolId: String!) {
rounds(
filter: { chainId: { equalTo: $chainId }, id: { equalTo: $alloPoolId } }
) {
roles {
address
}
}
}
`;

export const getRoundWithApplications = gql`
query RoundApplications($chainId: Int!, $roundId: String!) {
rounds(
Expand Down
8 changes: 8 additions & 0 deletions src/ext/indexer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,11 @@ export interface ApplicationWithRound {
export interface ApplicationRoundQueryResponse {
application: ApplicationWithRound;
}

export interface ManagerRolesResponse {
rounds: Array<{
roles: Array<{
address: string;
}>;
}>;
}
10 changes: 9 additions & 1 deletion src/routes/evaluationRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const router = Router();
* chainId:
* type: integer
* example: 42161
* signature:
* type: string
* example: "0x1234567890abcdef"
* example:
* alloPoolId: "609"
* alloApplicationId: "0"
Expand All @@ -70,6 +73,7 @@ const router = Router();
* answerEnum: 2
* summary: "The application is well-rounded, but some aspects need improvement."
* chainId: 42161
* signature: "0xdeadbeef"
* responses:
* 200:
* description: "Evaluation successfully created"
Expand Down Expand Up @@ -113,10 +117,14 @@ router.post('/', evaluateApplication);
* alloApplicationId:
* type: string
* example: "1"
* signature:
* type: string
* example: "0x1234567890abcdef"
* example:
* chainId: 42161
* alloPoolId: "609"
* alloApplicationId: "1"
* signature: "0xdeadbeef"
* responses:
* 200:
* description: "LLM evaluation triggered successfully"
Expand All @@ -130,7 +138,7 @@ router.post('/', evaluateApplication);
* example: "LLM evaluation triggered successfully"
* evaluationId:
* type: string
* example: "evalLLM123456"
* example: "1"
* 404:
* description: "Application not found"
* 500:
Expand Down
3 changes: 1 addition & 2 deletions src/service/EvaluationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ class EvaluationService {
): Promise<PromptEvaluationQuestions> => {
const questions = await evaluationQuestionRepository.find({
where: {
pool: { alloPoolId },
poolId: chainId,
pool: { alloPoolId, chainId },
},
relations: ['pool'],
order: {
Expand Down
45 changes: 45 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createLogger } from '@/logger';
import { type Request, type Response } from 'express';
import { validationResult } from 'express-validator';
import deterministicHash from 'deterministic-object-hash';
import { type Hex, keccak256, recoverAddress, toHex } from 'viem';
import { indexerClient } from './ext/indexer';
import { env } from './env';

const logger = createLogger();

Expand Down Expand Up @@ -33,3 +37,44 @@ export const addressFrom = (index: number): string => {
const address = index.toString(16).padStart(40, '0');
return `0x${address}`;
};

async function deterministicKeccakHash<T>(obj: T): Promise<Hex> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that's the best way to solve this

const hash = await deterministicHash(obj);
return keccak256(toHex(hash));
}

export async function recoverSignerAddress<T>(
obj: T,
signature: Hex
): Promise<Hex> {
return await recoverAddress({
hash: await deterministicKeccakHash(obj),
signature,
});
}

export async function isPoolManager<T>(
obj: T,
signature: Hex,
chainId: number,
alloPoolId: string
): Promise<boolean> {
const validAddresses = await indexerClient.getRoundManager({
chainId,
alloPoolId,
});
if (env.NODE_ENV === 'development' && signature === '0xdeadbeef') {
logger.info('Skipping signature check in development mode');
return true;
}
try {
const address = await recoverSignerAddress(obj, signature);
logger.info(`Recovered address: ${address}`);
return validAddresses.some(
addr => addr.toLowerCase() === address.toLowerCase()
);
} catch {
logger.warn('Failed to recover signer address');
return false;
}
}
Loading