transfer from monorepo
This commit is contained in:
36
worker/src/utils/anthropicCheck.ts
Normal file
36
worker/src/utils/anthropicCheck.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export function isValidAnthropicApiKey(key: string) {
|
||||
const regex = /^sk-ant-[a-zA-Z0-9_-]{32,}$/;
|
||||
return regex.test(key);
|
||||
}
|
||||
|
||||
export async function checkAnthropicAPIKey(apiKey: string) {
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-3-opus-20240229', // or a cheaper model
|
||||
max_tokens: 1, // minimal usage
|
||||
messages: [{ role: 'user', content: 'Hi' }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log('✅ API key is valid and has credit.');
|
||||
return true;
|
||||
} else {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (response.status === 401) {
|
||||
console.log('❌ Invalid API key.');
|
||||
} else if (response.status === 403 && data.error?.message?.includes('billing')) {
|
||||
console.log('❌ API key has no credit or is not authorized.');
|
||||
} else {
|
||||
console.log('⚠️ Unexpected error:', data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
57
worker/src/utils/constant.ts
Normal file
57
worker/src/utils/constant.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
|
||||
export const status = {
|
||||
ISSUE_FAILED_TO_BE_SUMMARIZED: "Issue failed to be summarized",
|
||||
ISSUE_SUCCESSFULLY_SUMMARIZED: "Issue successfully summarized",
|
||||
NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "No issues pending to be summarized",
|
||||
ROUND_LESS_THAN_OR_EQUAL_TO_1: "Round <= 1",
|
||||
NO_ORCA_CLIENT: "No orca client",
|
||||
NO_CHOSEN_AS_ISSUE_SUMMARIZER: "No chosen as issue summarizer",
|
||||
UNKNOWN_ERROR: "Unknown error",
|
||||
STAR_ISSUE_FAILED: "Star issue failed",
|
||||
GITHUB_CHECK_FAILED: "GitHub check failed",
|
||||
ANTHROPIC_API_KEY_INVALID: "Anthropic API key invalid",
|
||||
ANTHROPIC_API_KEY_NO_CREDIT: "Anthropic API key has no credit",
|
||||
NO_DATA_FOR_THIS_ROUND: "No data for this round",
|
||||
ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "Issue failed to add PR to summarizer todo",
|
||||
}
|
||||
|
||||
export const errorMessage = {
|
||||
ISSUE_FAILED_TO_BE_SUMMARIZED: "We couldn't summarize this issue. Please try again later.",
|
||||
ISSUE_SUCCESSFULLY_SUMMARIZED: "The issue was successfully summarized.",
|
||||
NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "There are no issues waiting to be summarized at this time.",
|
||||
ROUND_LESS_THAN_OR_EQUAL_TO_1: "This operation requires a round number greater than 1.",
|
||||
NO_ORCA_CLIENT: "The Orca client is not available.",
|
||||
NO_CHOSEN_AS_ISSUE_SUMMARIZER: "You haven't been selected as an issue summarizer.",
|
||||
UNKNOWN_ERROR: "An unexpected error occurred. Please try again later.",
|
||||
STAR_ISSUE_FAILED: "We couldn't star the issue. Please try again later.",
|
||||
GITHUB_CHECK_FAILED: "The GitHub check failed. Please verify your GitHub Key.",
|
||||
ANTHROPIC_API_KEY_INVALID: "The Anthropic API Key is not valid. Please check your API key.",
|
||||
ANTHROPIC_API_KEY_NO_CREDIT: "Your Anthropic API key has no remaining credits.",
|
||||
NO_DATA_FOR_THIS_ROUND: "There is no data available for this round.",
|
||||
ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "We couldn't add the PR to the summarizer todo list.",
|
||||
}
|
||||
|
||||
export const actionMessage = {
|
||||
ISSUE_FAILED_TO_BE_SUMMARIZED: "We couldn't summarize this issue. Please try again later.",
|
||||
ISSUE_SUCCESSFULLY_SUMMARIZED: "The issue was successfully summarized.",
|
||||
NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "There are no issues waiting to be summarized at this time.",
|
||||
ROUND_LESS_THAN_OR_EQUAL_TO_1: "This operation requires a round number greater than 1.",
|
||||
NO_ORCA_CLIENT: "Please click Orca icon to connect your Orca Pod.",
|
||||
NO_CHOSEN_AS_ISSUE_SUMMARIZER: "You haven't been selected as an issue summarizer.",
|
||||
UNKNOWN_ERROR: "An unexpected error occurred. Please try again later.",
|
||||
STAR_ISSUE_FAILED: "We couldn't star the issue. Please try again later.",
|
||||
GITHUB_CHECK_FAILED: "Please go to the env variable page to update your GitHub Key.",
|
||||
ANTHROPIC_API_KEY_INVALID: "Please follow the guide under task description page to set up your Anthropic API key correctly.",
|
||||
ANTHROPIC_API_KEY_NO_CREDIT: "Please add credits to continue.",
|
||||
NO_DATA_FOR_THIS_ROUND: "There is no data available for this round.",
|
||||
ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "We couldn't add the PR to the summarizer todo list. Please try again later.",
|
||||
}
|
||||
/*********************THE CONSTANTS THAT PROD/TEST ARE DIFFERENT *********************/
|
||||
export const defaultBountyMarkdownFile = "https://raw.githubusercontent.com/koii-network/prometheus-swarm-bounties/master/README.md"
|
||||
|
||||
export const customReward = 400*10**9 // This should be in ROE!
|
||||
|
||||
export const middleServerUrl = "https://ooww84kco0s0cs808w8cg804.dev.koii.network"
|
96
worker/src/utils/distributionList.ts
Normal file
96
worker/src/utils/distributionList.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { namespaceWrapper } from "@_koii/namespace-wrapper";
|
||||
import { getFile } from "./ipfs";
|
||||
|
||||
/**
|
||||
* Filter out ineligible nodes from the distribution list
|
||||
* @param distributionList Raw distribution list from namespace
|
||||
* @param submissions List of submissions for the round
|
||||
* @returns Filtered distribution list containing only eligible nodes
|
||||
*/
|
||||
async function filterIneligibleNodes(
|
||||
distributionList: Record<string, number>,
|
||||
roundNumber: number,
|
||||
): Promise<Record<string, any>> {
|
||||
const filteredDistributionList: Record<string, any> = {};
|
||||
|
||||
if (Object.keys(distributionList).length === 0) {
|
||||
console.log("Distribution list is empty, skipping filterIneligibleNodes");
|
||||
return filteredDistributionList;
|
||||
}
|
||||
|
||||
const taskSubmissionInfo = await namespaceWrapper.getTaskSubmissionInfo(roundNumber);
|
||||
if (!taskSubmissionInfo) {
|
||||
console.log("Task submission info is null, skipping filterIneligibleNodes");
|
||||
return filteredDistributionList;
|
||||
}
|
||||
|
||||
const submissions = taskSubmissionInfo.submissions;
|
||||
|
||||
for (const [stakingKey, amount] of Object.entries(distributionList)) {
|
||||
const numericAmount = amount as number;
|
||||
|
||||
// Skip if amount is zero or negative (failed audit)
|
||||
if (numericAmount <= 0) {
|
||||
console.log("Skipping staking key:", stakingKey, "Amount:", numericAmount);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find corresponding submission
|
||||
const submissionCID = submissions[roundNumber][stakingKey]["submission_value"];
|
||||
|
||||
const submission = await getFile(submissionCID);
|
||||
|
||||
// Skip if no submission found
|
||||
if (!submission) {
|
||||
console.log("No submission found, skipping staking key:", stakingKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
const submissionData = JSON.parse(submission);
|
||||
|
||||
console.log("Staking key:", stakingKey, "Submission data:", submissionData);
|
||||
|
||||
const payload = await namespaceWrapper.verifySignature(submissionData.signature, stakingKey);
|
||||
console.log("Payload:", payload);
|
||||
|
||||
const payloadData = JSON.parse(payload.data || "{}");
|
||||
|
||||
// Skip if submission has no PR URL or is a dummy submission
|
||||
if (!payloadData.prUrl || payloadData.prUrl === "none") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Node is eligible, include in filtered list
|
||||
filteredDistributionList[stakingKey] = payloadData;
|
||||
}
|
||||
|
||||
console.log("Filtered distribution list:", filteredDistributionList);
|
||||
|
||||
return filteredDistributionList;
|
||||
}
|
||||
|
||||
export async function getDistributionList(roundNumber: number): Promise<Record<string, any> | null> {
|
||||
try {
|
||||
const taskDistributionInfo = await namespaceWrapper.getTaskDistributionInfo(roundNumber);
|
||||
if (!taskDistributionInfo) {
|
||||
console.log("Task distribution info is null, skipping task");
|
||||
return null;
|
||||
}
|
||||
const distribution = taskDistributionInfo.distribution_rewards_submission[roundNumber];
|
||||
const leaderStakingKey = Object.keys(distribution)[0];
|
||||
console.log("Fetching distribution list for round", roundNumber, "with leader staking key", leaderStakingKey);
|
||||
const distributionList = await namespaceWrapper.getDistributionList(leaderStakingKey, roundNumber);
|
||||
if (!distributionList) {
|
||||
console.log("Distribution list is null, skipping task");
|
||||
return null;
|
||||
}
|
||||
console.log("Raw distribution list:", distributionList);
|
||||
|
||||
const parsedDistributionList: Record<string, number> = JSON.parse(distributionList);
|
||||
|
||||
return await filterIneligibleNodes(parsedDistributionList, roundNumber);
|
||||
} catch (error) {
|
||||
console.error("Error fetching distribution list:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
161
worker/src/utils/existingIssues.ts
Normal file
161
worker/src/utils/existingIssues.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { defaultBountyMarkdownFile } from "./constant";
|
||||
|
||||
interface BountyIssue {
|
||||
githubUrl: string;
|
||||
projectName: string;
|
||||
bountyTask: string;
|
||||
description: string;
|
||||
bountyAmount: string;
|
||||
bountyType: string;
|
||||
transactionHash: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export async function getExistingIssues(): Promise<BountyIssue[]> {
|
||||
try {
|
||||
// read from the bounty markdown file
|
||||
// console.log('Fetching markdown file from:', defaultBountyMarkdownFile);
|
||||
const bountyMarkdownFile = await fetch(defaultBountyMarkdownFile);
|
||||
const bountyMarkdownFileText = await bountyMarkdownFile.text();
|
||||
|
||||
// console.log('Raw markdown content:', bountyMarkdownFileText);
|
||||
|
||||
const bountyMarkdownFileLines = bountyMarkdownFileText.split("\n");
|
||||
// console.log('Number of lines:', bountyMarkdownFileLines.length);
|
||||
|
||||
const issues: BountyIssue[] = [];
|
||||
let isTableStarted = false;
|
||||
|
||||
for (const line of bountyMarkdownFileLines) {
|
||||
// Skip empty lines
|
||||
if (line.trim() === '') {
|
||||
// console.log('Skipping empty line');
|
||||
continue;
|
||||
}
|
||||
|
||||
// console.log('Processing line:', line);
|
||||
|
||||
// Skip the title line starting with #
|
||||
if (line.startsWith('#')) {
|
||||
// console.log('Found title line:', line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the header and separator lines
|
||||
if (line.startsWith('|') && line.includes('GitHub URL')) {
|
||||
//console.log('Found header line');
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('|') && line.includes('-----')) {
|
||||
// console.log('Found separator line');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process table rows
|
||||
if (line.startsWith('|')) {
|
||||
isTableStarted = true;
|
||||
// Remove first and last | and split by |
|
||||
const cells = line.slice(1, -1).split('|').map(cell => cell.trim());
|
||||
// console.log('Parsed cells:', cells);
|
||||
|
||||
// Extract GitHub URL and name from markdown link format [name](url)
|
||||
const githubUrlMatch = cells[0].match(/\[(.*?)\]\((.*?)\)/);
|
||||
// console.log('GitHub URL match:', githubUrlMatch);
|
||||
|
||||
const projectName = githubUrlMatch ? githubUrlMatch[1] : '';
|
||||
const githubUrl = githubUrlMatch ? githubUrlMatch[2] : '';
|
||||
|
||||
const issue: BountyIssue = {
|
||||
githubUrl,
|
||||
projectName,
|
||||
bountyTask: cells[1],
|
||||
description: cells[3],
|
||||
bountyAmount: cells[4],
|
||||
bountyType: cells[5],
|
||||
transactionHash: cells[6],
|
||||
status: cells[7]
|
||||
};
|
||||
|
||||
// console.log('Created issue object:', issue);
|
||||
issues.push(issue);
|
||||
}
|
||||
}
|
||||
// Filter all issues with status "Initialized" && Bounty Task is Document & Summarize
|
||||
console.log('Final parsed issues number:', issues.length);
|
||||
return issues
|
||||
} catch (error) {
|
||||
// console.error('Error processing markdown:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function getInitializedDocumentSummarizeIssues(issues: BountyIssue[]) {
|
||||
|
||||
return issues.filter(issue => issue.status === "Initialized" && issue.bountyTask === "Document & Summarize");
|
||||
}
|
||||
|
||||
|
||||
// async function main(){
|
||||
// const existingIssues = await getExistingIssues();
|
||||
// const transactionHashs = [
|
||||
// "51680569890c40efa0f1f891044db219",
|
||||
// "21a7021da88a4092af014702da7638cb",
|
||||
// "befcf8d281074e3e934d8947c02ecb6f",
|
||||
// "a1db701bbda24a45b573e58840d9b31c",
|
||||
// "4ab503566a1142b1a3a9b406849839c9",
|
||||
// "7f6fb74e4b6a41b0af805ca3f6c9ea15",
|
||||
// "878af0d284c7460394b6d6e1090119be",
|
||||
// "64d90b6f891d4ea385c8f6ad81808103",
|
||||
// "6f7522b2e2374d4ca4f92bcf1f694bec",
|
||||
// "e85201ae9ed9417e8c56216bb44cd78b",
|
||||
// "d2ca259ef6ce4129a786677d919aad24",
|
||||
// "6ce684318aab4356b76ba64e87b31be7",
|
||||
// "d94d07647b1b42819d9bf629f5624ae1",
|
||||
// "60aa8f04dd314c14b30e5ac2957bd9f8",
|
||||
// "b7e21455e41b4626b5015b7bf39ff190",
|
||||
// "5e7109ed4dd94373958eda2416337ad3",
|
||||
// "2d647d3ab2c5465890939315ada47fd7",
|
||||
// "51ade1ba2f6341e99aa6ec56b1a00f27",
|
||||
// "a74f5e80238a4582aa444c18e9d5d66f",
|
||||
// "8390a3143a8445f196a124605e524f3d",
|
||||
// "26b712f341ca457d86db67ecd841c438",
|
||||
// "0ec98ba1e7174eef87772df8356bab0d",
|
||||
// "2737c33bff8c4490b7e5f53a5f5da580",
|
||||
// "e5b9b714d5694680a56cfa77361f3477",
|
||||
// "afb1bbbf1c074d28bef5fa216008cd6b",
|
||||
// "b40da8c53a644a6e898e3314e08c10ea",
|
||||
// "6a2f743c0497427ea4cd3cadb785b166",
|
||||
// "ce390111854b4a4b980b5e1e3f7c2f0e",
|
||||
// "c1b54e7a8dfd40be873051dd64bae5c4",
|
||||
// "7dcda8e5969c45e08f9a8887d8c39d10",
|
||||
// "fc11382529644d55b95fc2264e40436f",
|
||||
// "7c145db039b64edba719e81dd398b37e",
|
||||
// "c92b4920b25540a692c3b8e12215f0e0",
|
||||
// "cebbf4e2310d4a11ac44321823ddb373",
|
||||
// "5ae707005d0e413cb9feb9bdadc1e987",
|
||||
// "d28f92643c2548338d3e49144bc66afc",
|
||||
// "bd18484224c24fc786a5171e9d06cd50",
|
||||
// "f0605ea0f9524572bbe5bf4e72597476",
|
||||
// "62e6303c57334f72ada393bfa9e7aacc",
|
||||
// "f4ee9168804c4b01932ac76cc32d1f13",
|
||||
// "d4a95e2d35db47d28a208309019b1925",
|
||||
// "014425adc1b8447ab34d7d8104e91cf0"
|
||||
// ]
|
||||
// const initializedDocumentSummarizeIssues = existingIssues.filter((issue) => transactionHashs.includes(issue.transactionHash));
|
||||
// if (initializedDocumentSummarizeIssues.length == 0) {
|
||||
// console.log("No issues pending to be summarized");
|
||||
// return;
|
||||
// }
|
||||
// console.log("Initialized Document & Summarize issues number:", initializedDocumentSummarizeIssues.length);
|
||||
// }
|
||||
// async function main() {
|
||||
// try {
|
||||
// const existingIssues = await getInitializedDocumentSummarizeIssues();
|
||||
// console.log('Initialized Document & Summarize issues number:', existingIssues.length);
|
||||
// } catch (error) {
|
||||
// console.error('Error in main:', error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// main();
|
36
worker/src/utils/githubCheck.ts
Normal file
36
worker/src/utils/githubCheck.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export async function checkGitHub(username: string, token: string) {
|
||||
// 1. Check username
|
||||
const userRes = await fetch(`https://api.github.com/users/${username}`);
|
||||
const isUsernameValid = userRes.status === 200;
|
||||
|
||||
// 2. Check token
|
||||
const tokenRes = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
},
|
||||
});
|
||||
const isTokenValid = tokenRes.status === 200;
|
||||
const isIdentityValid = await checkGitHubIdentity(username, token);
|
||||
return isIdentityValid&&isUsernameValid&&isTokenValid
|
||||
}
|
||||
|
||||
async function checkGitHubIdentity(username: string, token: string) {
|
||||
const res = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
return false
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.login.toLowerCase() !== username.toLowerCase()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
34
worker/src/utils/ipfs.ts
Normal file
34
worker/src/utils/ipfs.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { namespaceWrapper } from "@_koii/namespace-wrapper";
|
||||
import { KoiiStorageClient } from "@_koii/storage-task-sdk";
|
||||
import fs from "fs";
|
||||
|
||||
export async function storeFile(data: any, filename: string = "submission.json"): Promise<string> {
|
||||
// Create a new instance of the Koii Storage Client
|
||||
const client = KoiiStorageClient.getInstance({});
|
||||
const basePath = await namespaceWrapper.getBasePath();
|
||||
try {
|
||||
// Write the data to a temp file
|
||||
fs.writeFileSync(`${basePath}/${filename}`, typeof data === "string" ? data : JSON.stringify(data));
|
||||
|
||||
// Get the user staking account, to be used for signing the upload request
|
||||
const userStaking = await namespaceWrapper.getSubmitterAccount();
|
||||
if (!userStaking) {
|
||||
throw new Error("No staking keypair found");
|
||||
}
|
||||
|
||||
// Upload the file to IPFS and get the CID
|
||||
const { cid } = await client.uploadFile(`${basePath}/${filename}`, userStaking);
|
||||
return cid;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
// Delete the temp file
|
||||
fs.unlinkSync(`${basePath}/${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFile(cid: string, filename: string = "submission.json"): Promise<string> {
|
||||
const storageClient = KoiiStorageClient.getInstance({});
|
||||
const fileBlob = await storageClient.getFile(cid, filename);
|
||||
return await fileBlob.text();
|
||||
}
|
265
worker/src/utils/leader.ts
Normal file
265
worker/src/utils/leader.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper";
|
||||
import { getFile } from "./ipfs";
|
||||
import seedrandom from "seedrandom";
|
||||
|
||||
export async function fetchRoundSubmissionGitHubRepoOwner(
|
||||
roundNumber: number,
|
||||
submitterPublicKey: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const taskSubmissionInfo = await namespaceWrapper.getTaskSubmissionInfo(roundNumber);
|
||||
if (!taskSubmissionInfo) {
|
||||
console.error("NO TASK SUBMISSION INFO");
|
||||
return null;
|
||||
}
|
||||
const submissions = taskSubmissionInfo.submissions;
|
||||
// This should only have one round
|
||||
const lastRound = Object.keys(submissions).pop();
|
||||
if (!lastRound) {
|
||||
return null;
|
||||
}
|
||||
const lastRoundSubmissions = submissions[lastRound];
|
||||
const lastRoundSubmitterSubmission = lastRoundSubmissions[submitterPublicKey];
|
||||
console.log("lastRoundSubmitterSubmission", { lastRoundSubmitterSubmission });
|
||||
if (!lastRoundSubmitterSubmission) {
|
||||
return null;
|
||||
}
|
||||
const cid = lastRoundSubmitterSubmission.submission_value;
|
||||
const submissionString = await getFile(cid);
|
||||
const submission = JSON.parse(submissionString);
|
||||
console.log({ submission });
|
||||
|
||||
// verify the signature of the submission
|
||||
const signaturePayload = await namespaceWrapper.verifySignature(submission.signature, submitterPublicKey);
|
||||
|
||||
console.log({ signaturePayload });
|
||||
|
||||
// verify the signature payload
|
||||
if (signaturePayload.error || !signaturePayload.data) {
|
||||
console.error("INVALID SIGNATURE");
|
||||
return null;
|
||||
}
|
||||
const data = JSON.parse(signaturePayload.data);
|
||||
|
||||
if (data.taskId !== TASK_ID || data.stakingKey !== submitterPublicKey) {
|
||||
console.error("INVALID SIGNATURE DATA");
|
||||
return null;
|
||||
}
|
||||
if (!data.githubUsername) {
|
||||
console.error("NO GITHUB USERNAME");
|
||||
console.log("data", { data });
|
||||
return null;
|
||||
}
|
||||
return data.githubUsername;
|
||||
} catch (error) {
|
||||
console.error("FETCH LAST ROUND SUBMISSION GITHUB REPO OWNER ERROR:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectShortestDistance(keys: string[], submitterPublicKey: string): Promise<string> {
|
||||
let shortestDistance = Infinity;
|
||||
let closestKey = "";
|
||||
for (const key of keys) {
|
||||
const distance = knnDistance(submitterPublicKey, key);
|
||||
if (distance < shortestDistance) {
|
||||
shortestDistance = distance;
|
||||
closestKey = key;
|
||||
}
|
||||
}
|
||||
return closestKey;
|
||||
}
|
||||
|
||||
async function getSubmissionInfo(roundNumber: number): Promise<any> {
|
||||
try {
|
||||
return await namespaceWrapper.getTaskSubmissionInfo(roundNumber);
|
||||
} catch (error) {
|
||||
console.error("GET SUBMISSION INFO ERROR:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function calculatePublicKeyFrequency(submissions: any): Record<string, number> {
|
||||
const frequency: Record<string, number> = {};
|
||||
for (const round in submissions) {
|
||||
for (const publicKey in submissions[round]) {
|
||||
if (frequency[publicKey]) {
|
||||
frequency[publicKey]++;
|
||||
} else {
|
||||
frequency[publicKey] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return frequency;
|
||||
}
|
||||
|
||||
function handleAuditTrigger(submissionAuditTrigger: any): Set<string> {
|
||||
const auditTriggerKeys = new Set<string>();
|
||||
for (const round in submissionAuditTrigger) {
|
||||
for (const publicKey in submissionAuditTrigger[round]) {
|
||||
auditTriggerKeys.add(publicKey);
|
||||
}
|
||||
}
|
||||
return auditTriggerKeys;
|
||||
}
|
||||
|
||||
async function selectLeaderKey(
|
||||
sortedKeys: string[],
|
||||
leaderNumber: number,
|
||||
submitterPublicKey: string,
|
||||
submissionPublicKeysFrequency: Record<string, number>,
|
||||
): Promise<string> {
|
||||
const topValue = sortedKeys[leaderNumber - 1];
|
||||
const count = sortedKeys.filter(
|
||||
(key) => submissionPublicKeysFrequency[key] >= submissionPublicKeysFrequency[topValue],
|
||||
).length;
|
||||
|
||||
if (count >= leaderNumber) {
|
||||
const rng = seedrandom(String(TASK_ID));
|
||||
const guaranteedKeys = sortedKeys.filter(
|
||||
(key) => submissionPublicKeysFrequency[key] > submissionPublicKeysFrequency[topValue],
|
||||
);
|
||||
const randomKeys = sortedKeys
|
||||
.filter((key) => submissionPublicKeysFrequency[key] === submissionPublicKeysFrequency[topValue])
|
||||
.sort(() => rng() - 0.5)
|
||||
.slice(0, leaderNumber - guaranteedKeys.length);
|
||||
const keys = [...guaranteedKeys, ...randomKeys];
|
||||
return await selectShortestDistance(keys, submitterPublicKey);
|
||||
} else {
|
||||
const keys = sortedKeys.slice(0, leaderNumber);
|
||||
return await selectShortestDistance(keys, submitterPublicKey);
|
||||
}
|
||||
}
|
||||
export async function getRandomNodes(roundNumber: number, numberOfNodes: number): Promise<string[]> {
|
||||
console.log("Getting random nodes for round:", roundNumber, "with number of nodes:", numberOfNodes);
|
||||
const lastRoundSubmission = await getSubmissionInfo(roundNumber - 1);
|
||||
console.log("Last round submission:", lastRoundSubmission);
|
||||
if (!lastRoundSubmission) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lastRoundSubmissions = lastRoundSubmission.submissions;
|
||||
console.log("Last round submissions:", lastRoundSubmissions);
|
||||
|
||||
// Get the last round number
|
||||
const lastRound = Object.keys(lastRoundSubmissions).pop();
|
||||
if (!lastRound) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get the submissions for that round
|
||||
const submissions = lastRoundSubmissions[lastRound];
|
||||
console.log("Submissions:", submissions);
|
||||
const availableKeys = Object.keys(submissions);
|
||||
console.log("Available keys:", availableKeys);
|
||||
// If we have fewer submissions than requested nodes, return all available submissions
|
||||
if (availableKeys.length <= numberOfNodes) {
|
||||
return availableKeys;
|
||||
}
|
||||
|
||||
const seed = TASK_ID + roundNumber.toString() || "default" + roundNumber;
|
||||
const rng = seedrandom(seed);
|
||||
// Use the keys from the submissions object
|
||||
const randomKeys = availableKeys.sort(() => rng() - 0.5).slice(0, numberOfNodes);
|
||||
|
||||
console.log("Random keys:", randomKeys);
|
||||
return randomKeys;
|
||||
}
|
||||
|
||||
// Helper function that finds the leader for a specific round
|
||||
async function getLeaderForRound(
|
||||
roundNumber: number,
|
||||
maxLeaderNumber: number,
|
||||
submitterPublicKey: string,
|
||||
): Promise<{ chosenKey: string | null; leaderNode: string | null }> {
|
||||
if (roundNumber <= 0) {
|
||||
return { chosenKey: null, leaderNode: null };
|
||||
}
|
||||
|
||||
const submissionPublicKeysFrequency: Record<string, number> = {};
|
||||
const submissionAuditTriggerKeys = new Set<string>();
|
||||
|
||||
for (let i = 1; i < 5; i++) {
|
||||
const taskSubmissionInfo = await getSubmissionInfo(roundNumber - i);
|
||||
console.log({ taskSubmissionInfo });
|
||||
if (taskSubmissionInfo) {
|
||||
const submissions = taskSubmissionInfo.submissions;
|
||||
const frequency = calculatePublicKeyFrequency(submissions);
|
||||
Object.assign(submissionPublicKeysFrequency, frequency);
|
||||
|
||||
const auditTriggerKeys = handleAuditTrigger(taskSubmissionInfo.submissions_audit_trigger);
|
||||
auditTriggerKeys.forEach((key) => submissionAuditTriggerKeys.add(key));
|
||||
}
|
||||
}
|
||||
|
||||
const keysNotInAuditTrigger = Object.keys(submissionPublicKeysFrequency).filter(
|
||||
(key) => !submissionAuditTriggerKeys.has(key),
|
||||
);
|
||||
const sortedKeys = keysNotInAuditTrigger.sort(
|
||||
(a, b) => submissionPublicKeysFrequency[b] - submissionPublicKeysFrequency[a],
|
||||
);
|
||||
|
||||
console.log({ sortedKeys });
|
||||
|
||||
let chosenKey = null;
|
||||
|
||||
const leaderNumber = sortedKeys.length < maxLeaderNumber ? sortedKeys.length : maxLeaderNumber;
|
||||
|
||||
chosenKey = await selectLeaderKey(sortedKeys, leaderNumber, submitterPublicKey, submissionPublicKeysFrequency);
|
||||
|
||||
// Find GitHub username for the chosen key
|
||||
for (let i = 1; i < 5; i++) {
|
||||
const githubUsername = await fetchRoundSubmissionGitHubRepoOwner(roundNumber - i, chosenKey);
|
||||
if (githubUsername) {
|
||||
return { chosenKey, leaderNode: githubUsername };
|
||||
}
|
||||
}
|
||||
|
||||
return { chosenKey, leaderNode: null };
|
||||
}
|
||||
|
||||
export async function getLeaderNode({
|
||||
roundNumber,
|
||||
leaderNumber = 5,
|
||||
submitterPublicKey,
|
||||
}: {
|
||||
roundNumber: number;
|
||||
leaderNumber?: number;
|
||||
submitterPublicKey: string;
|
||||
}): Promise<{ isLeader: boolean; leaderNode: string | null }> {
|
||||
// Find leader for current round
|
||||
const currentLeader = await getLeaderForRound(roundNumber, leaderNumber, submitterPublicKey);
|
||||
console.log({ currentLeader });
|
||||
|
||||
if (currentLeader.chosenKey === submitterPublicKey) {
|
||||
// If we're the leader, get the leader from 3 rounds ago
|
||||
const previousLeader = await getLeaderForRound(roundNumber - 3, leaderNumber, submitterPublicKey);
|
||||
console.log({ previousLeader });
|
||||
return { isLeader: true, leaderNode: previousLeader.leaderNode };
|
||||
}
|
||||
|
||||
// Not the leader, return the current leader's info
|
||||
return { isLeader: false, leaderNode: currentLeader.leaderNode };
|
||||
}
|
||||
|
||||
function base58ToNumber(char: string): number {
|
||||
const base58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
return base58Chars.indexOf(char);
|
||||
}
|
||||
|
||||
function knnDistance(a: string, b: string): number {
|
||||
if (a.length !== b.length) {
|
||||
throw new Error("Strings must be of the same length for KNN distance calculation.");
|
||||
}
|
||||
const truncatedA = a.slice(0, 30);
|
||||
const truncatedB = b.slice(0, 30);
|
||||
|
||||
let distance = 0;
|
||||
for (let i = 0; i < truncatedA.length; i++) {
|
||||
const numA = base58ToNumber(truncatedA[i]);
|
||||
const numB = base58ToNumber(truncatedB[i]);
|
||||
distance += Math.abs(numA - numB);
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
40
worker/src/utils/submissionJSONSignatureDecode.ts
Normal file
40
worker/src/utils/submissionJSONSignatureDecode.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { TASK_ID } from "@_koii/namespace-wrapper";
|
||||
import { getFile } from "./ipfs";
|
||||
import { Submission } from "@_koii/namespace-wrapper/dist/types";
|
||||
import { Submitter } from "@_koii/task-manager/dist/types/global";
|
||||
import { namespaceWrapper } from "@_koii/namespace-wrapper";
|
||||
export async function submissionJSONSignatureDecode({submission_value, submitterPublicKey, roundNumber}: {submission_value: string, submitterPublicKey: string, roundNumber: number}) {
|
||||
let submissionString;
|
||||
try {
|
||||
console.log("Getting file from IPFS", submission_value);
|
||||
submissionString = await getFile(submission_value);
|
||||
console.log("submissionString", submissionString);
|
||||
} catch (error) {
|
||||
|
||||
console.log("error", error);
|
||||
console.error("INVALID SIGNATURE DATA");
|
||||
return null;
|
||||
}
|
||||
// verify the signature of the submission
|
||||
const submission = JSON.parse(submissionString);
|
||||
console.log("submission", submission);
|
||||
const signaturePayload = await namespaceWrapper.verifySignature(submission.signature, submitterPublicKey);
|
||||
if (!signaturePayload.data) {
|
||||
console.error("INVALID SIGNATURE");
|
||||
return null;
|
||||
}
|
||||
const data = JSON.parse(signaturePayload.data);
|
||||
console.log("signaturePayload", signaturePayload);
|
||||
console.log("data", data);
|
||||
if (
|
||||
data.taskId !== TASK_ID ||
|
||||
data.roundNumber !== roundNumber ||
|
||||
data.stakingKey !== submitterPublicKey ||
|
||||
!data.pubKey ||
|
||||
!data.prUrl
|
||||
) {
|
||||
console.error("INVALID SIGNATURE DATA");
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
}
|
Reference in New Issue
Block a user