From 6cf72f7632b8ea298cf6fe823159bb3d18b8e19d Mon Sep 17 00:00:00 2001 From: ethan Date: Tue, 12 Nov 2024 15:07:09 -0500 Subject: [PATCH 01/80] added --- .gitignore | 51 +++- docker-compose.yml | 48 +++ execution-role-policy.json | 12 + orchestrator/Dockerfile | 23 ++ orchestrator/README.md | 6 + orchestrator/package.json | 26 ++ orchestrator/src/app.ts | 286 ++++++++++++++++++ orchestrator/tsconfig.json | 16 + task-definition.json | 76 +++++ task-role-policy.json | 12 + verifiable-delay-function/Dockerfile | 35 +++ verifiable-delay-function/main.py | 18 +- .../verifiable_delay_function_converter.py | 1 + 13 files changed, 600 insertions(+), 10 deletions(-) create mode 100644 docker-compose.yml create mode 100644 execution-role-policy.json create mode 100644 orchestrator/Dockerfile create mode 100644 orchestrator/package.json create mode 100644 orchestrator/src/app.ts create mode 100644 orchestrator/tsconfig.json create mode 100644 task-definition.json create mode 100644 task-role-policy.json create mode 100644 verifiable-delay-function/Dockerfile diff --git a/.gitignore b/.gitignore index eba74f4..77d110f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,50 @@ -venv/ \ No newline at end of file +# Ignore virtual environments +venv/ +.env/ +.env +env + +# Ignore distribution/build directories +dist/ +build/ +*.egg-info/ +*.pyc +__pycache__/ + +# Ignore Node.js dependencies +node_modules/ +**/node_modules/ + +# Ignore Terraform and related files +terraform/ +**/.terraform/ +*.tfstate +*.tfstate.backup + +# Ignore package locks and dependency files +package-lock.json +yarn.lock + +# Ignore OS-generated files +.DS_Store +Thumbs.db + +# Ignore editor-specific files +.idea/ +.vscode/ + +# Ignore logs and temporary files +logs/ +*.log +temp/ +.tmp/ +*.swp + +# Ignore Python bytecode and cache files +__pycache__/ +*.py[cod] + +# Ignore Git-specific large files +*.pack +*.idx + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1341f34 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + postgres: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER:-myuser} + POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword} + POSTGRES_DB: ${DB_NAME:-mydatabase} + ports: + - "5432:5432" + networks: + - backend + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-mydatabase}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator: + image: randao/orchestrator:v0.1.3 + depends_on: + postgres: + condition: service_healthy + environment: + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: ${DB_USER:-myuser} + DB_PASSWORD: ${DB_PASSWORD:-mypassword} + DB_NAME: ${DB_NAME:-mydatabase} + ENVIRONMENT: local + DOCKER_NETWORK: backend # Passing the network name + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket + +networks: + backend: + name: backend # This will set the network name explicitly + driver: bridge + + +volumes: + pgdata: + driver: local diff --git a/execution-role-policy.json b/execution-role-policy.json new file mode 100644 index 0000000..b833d12 --- /dev/null +++ b/execution-role-policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} diff --git a/orchestrator/Dockerfile b/orchestrator/Dockerfile new file mode 100644 index 0000000..5c17fc9 --- /dev/null +++ b/orchestrator/Dockerfile @@ -0,0 +1,23 @@ +# Use the official Node.js image as the base image +FROM node:16 + +# Create and set the working directory +WORKDIR /usr/src/app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the entire project into the container +COPY . . + +# Compile TypeScript to JavaScript +RUN npx tsc + +# Expose the port if needed +EXPOSE 3000 + +# Run the compiled app +CMD ["node", "dist/app.js"] diff --git a/orchestrator/README.md b/orchestrator/README.md index e69de29..14ca0d2 100644 --- a/orchestrator/README.md +++ b/orchestrator/README.md @@ -0,0 +1,6 @@ +cd into repo + +docker login +docker build -t satoshispalace/orchestrator:latest -t satoshispalace/orchestrator:v0.0.30 . +docker push satoshispalace/orchestrator:v0.0.30 +docker push satoshispalace/orchestrator:latest diff --git a/orchestrator/package.json b/orchestrator/package.json new file mode 100644 index 0000000..19385c4 --- /dev/null +++ b/orchestrator/package.json @@ -0,0 +1,26 @@ +{ + "devDependencies": { + "@types/dockerode": "^3.3.31", + "@types/node": "^22.9.0", + "@types/pg": "^8.11.10", + "serverless-offline": "^14.3.3", + "typescript": "^5.6.3" + }, + "dependencies": { + "aws-sdk": "^2.1692.0", + "axios": "^1.7.7", + "crypto": "^1.0.1", + "dockerode": "^4.0.2", + "pg": "^8.13.1" + }, + "name": "ao-vrf", + "description": "1. To build:\r ```\r docker build -t serverless-multi-cloud .\r ```", + "version": "1.0.0", + "main": "Organizer.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts new file mode 100644 index 0000000..0e53d17 --- /dev/null +++ b/orchestrator/src/app.ts @@ -0,0 +1,286 @@ +import { Client } from 'pg'; +import Docker from 'dockerode'; +import AWS from 'aws-sdk'; +const docker = new Docker(); + +// Database configuration +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + user: process.env.DB_USER || 'myuser', + password: process.env.DB_PASSWORD || 'mypassword', + database: process.env.DB_NAME || 'mydatabase', +}; + +// Constants for configuration +const POLLING_INTERVAL_MS = 100; +const MINIMUM_ENTRIES = 50; +const TARGET_ENTRIES = 75; +const DROP_CHANCE = 0.005; +//Expected increments per second=10×0.005=0.05 +//180 times per hour +//4,320 times per day +//1,576,800 times per year +const MAX_OUTSTANDING_REQUESTS = 10; +const MAX_RETRIES = 10; +const RETRY_DELAY_MS = 10000; +const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.0'; +const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; +const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); +const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks +const ongoingContainers = new Set(); // Track container IDs of running Docker containers + +let ongoingRequest = false; + +// Retry logic for connecting to PostgreSQL +async function connectWithRetry(): Promise { + const client = new Client(dbConfig); + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + await client.connect(); + console.log(`Connected to PostgreSQL database (Attempt ${attempt})`); + return client; + } catch (error) { + console.error(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + } + } + throw new Error("Failed to connect to PostgreSQL after multiple attempts"); +} + +// Setup the `verifiable_delay_functions` table if not exists +async function setupDatabase(client: Client): Promise { + await client.query(` + CREATE TABLE IF NOT EXISTS verifiable_delay_functions ( + id TEXT PRIMARY KEY, -- Define as TEXT to match UUID format + request_id TEXT, + modulus TEXT NOT NULL, + input TEXT NOT NULL, + output TEXT NOT NULL, + proof JSON NOT NULL, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + console.log("Database setup complete. 'verifiable_delay_functions' table is ready."); +} + +// Added variable to track the number of Spot instance interruptions +let spotInterruptions = 0; + +// Modified function to trigger VDF job pod using ECS or Docker +async function triggerVDFJobPod(): Promise { + if (ENVIRONMENT === 'cloud') { + try { + console.log("Cloud environment detected. Launching ECS task."); + const result = await ecs.runTask({ + cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', + taskDefinition: 'vdf-job', + capacityProviderStrategy: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 1 + } + ], + networkConfiguration: { + awsvpcConfiguration: { + subnets: [process.env.SUBNET_ID || 'subnet-12345678'], + securityGroups: [process.env.SECURITY_GROUP || 'sg-12345678'], + assignPublicIp: 'ENABLED' + } + }, + overrides: { + containerOverrides: [ + { + name: 'vdf_job_container', + environment: [ + { name: 'DATABASE_TYPE', value: 'postgresql' }, + { name: 'DATABASE_HOST', value: process.env.DB_HOST || 'cloud-postgres-host' }, + { name: 'DATABASE_PORT', value: process.env.DB_PORT || '5432' }, + { name: 'DATABASE_USER', value: process.env.DB_USER || 'myuser' }, + { name: 'DATABASE_PASSWORD', value: process.env.DB_PASSWORD || 'mypassword' }, + { name: 'DATABASE_NAME', value: process.env.DB_NAME || 'mydatabase' }, + ] + } + ] + }, + count: 1 + }).promise(); + + const taskArn = result.tasks?.[0]?.taskArn; + if (taskArn) { + ongoingTasks.add(taskArn); + console.log(`ECS task started successfully: ${taskArn}`); + return taskArn; + } + return null; + } catch (error: any) { + if (error?.code === 'SpotCapacityNotAvailableException') { + spotInterruptions++; + console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); + } else { + console.error("Error launching ECS task:", error); + } + return null; + } + } else { + const containerName = `vdf_job_${Date.now()}`; + console.log(`Starting Docker container with name: ${containerName}`); + const container = await docker.createContainer({ + Image: VDF_JOB_IMAGE, + Cmd: ['python', 'main.py'], + Env: [ + `DATABASE_TYPE=postgresql`, + `DATABASE_HOST=${dbConfig.host}`, + `DATABASE_PORT=${dbConfig.port.toString()}`, + `DATABASE_USER=${dbConfig.user}`, + `DATABASE_PASSWORD=${dbConfig.password}`, + `DATABASE_NAME=${dbConfig.database}`, + ], + HostConfig: { + NetworkMode: 'backend', + }, + name: containerName + }); + + await container.start(); + ongoingContainers.add(container.id); + console.log(`Docker container ${containerName} started successfully.`); + return container.id; + } +} + +// Modified function to wait for ECS tasks to complete and remove them from tracking +async function monitorECSTasks(): Promise { + if (ongoingTasks.size === 0) return; + + const describeTasksResult = await ecs.describeTasks({ + cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', + tasks: Array.from(ongoingTasks) + }).promise(); + + describeTasksResult.tasks?.forEach(task => { + // Check capacity provider name to determine if it is running Fargate or Fargate Spot + const capacityProvider = task.capacityProviderName || 'Unknown'; + + console.log(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); + + if (task.lastStatus === 'STOPPED') { + console.log(`ECS task stopped: ${task.taskArn}`); + if (task.stoppedReason) { + console.log(`Task stopped reason: ${task.stoppedReason}`); + if (task.stoppedReason.includes('Host EC2 instance termination')) { + spotInterruptions++; + console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); + } + } else { + console.log('Task stopped due to normal completion.'); + } + ongoingTasks.delete(task.taskArn as string); + } + }); +} + + + + +// Function to wait for Docker containers to complete and remove them from tracking +async function monitorDockerContainers(): Promise { + if (ongoingContainers.size === 0) return; + + for (const containerId of ongoingContainers) { + try { + const container = docker.getContainer(containerId); + const containerInfo = await container.inspect(); + + if (containerInfo.State.Status === 'exited') { + console.log(`Docker container stopped: ${containerId}`); + await container.remove(); + ongoingContainers.delete(containerId); + } + } catch (error) { + console.error(`Error monitoring Docker container ${containerId}:`, error); + ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) + } + } +} + +// Function to check if there are fewer than MINIMUM_ENTRIES and fetch until TARGET_ENTRIES +async function checkAndFetchIfNeeded(client: Client): Promise { + try { + if (ongoingRequest) return; + + const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); + const currentCount = parseInt(res.rows[0].count, 10); + + if (currentCount < MINIMUM_ENTRIES) { + const entriesNeeded = TARGET_ENTRIES - currentCount; + console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries to reach ${TARGET_ENTRIES}...`); + ongoingRequest = true; + let tasksTriggered = 0; + + while (tasksTriggered < entriesNeeded) { + if (ENVIRONMENT === 'cloud' && ongoingTasks.size >= MAX_OUTSTANDING_REQUESTS) { + await monitorECSTasks(); + } else if (ENVIRONMENT !== 'cloud' && ongoingContainers.size >= MAX_OUTSTANDING_REQUESTS) { + await monitorDockerContainers(); + } else { + const taskArn = await triggerVDFJobPod(); + if (taskArn) { + tasksTriggered++; + console.log(`Task triggered: ${taskArn}. Total ongoing tasks: ${ongoingTasks.size}. Total ongoing containers ${+ ongoingContainers.size}`); + } + } + } + ongoingRequest = false; + } + } catch (error) { + console.error('Error during check and fetch:', error); + ongoingRequest = false; + } +} + +// Polling function to manage entries and delete old ones occasionally +async function polling(client: Client): Promise { + if (Math.random() < DROP_CHANCE) { + try { + console.log("Randomly chosen to log and delete the oldest entry..."); + const res = await client.query('SELECT * FROM verifiable_delay_functions ORDER BY id ASC LIMIT 1'); + const entry = res.rows[0]; + + if (entry) { + console.log("Logging and deleting oldest entry:", JSON.stringify(entry, null, 2)); + await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [entry.id]); + console.log("Oldest entry deleted from database."); + } else { + console.log("No entries available to delete."); + } + } catch (error) { + console.error('Error logging and deleting oldest entry:', error); + } + } else { + await checkAndFetchIfNeeded(client); + } +} + +// Main function +async function run(): Promise { + const client = await connectWithRetry(); + await setupDatabase(client); + + setInterval(async () => { + await polling(client); + }, POLLING_INTERVAL_MS); + + setInterval(async () => { + const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); + console.log(`Periodic log - Current database size: ${res.rows[0].count}`); + }, 10000); + + process.on("SIGTERM", async () => { + console.log("SIGTERM received. Closing database connection."); + await client.end(); + process.exit(0); + }); +} + +run().catch((err) => console.error(`Error in main function: ${err}`)); diff --git a/orchestrator/tsconfig.json b/orchestrator/tsconfig.json new file mode 100644 index 0000000..493e143 --- /dev/null +++ b/orchestrator/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "commonjs", + "target": "es6", + "strict": true, + "esModuleInterop": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/task-definition.json b/task-definition.json new file mode 100644 index 0000000..cf7e617 --- /dev/null +++ b/task-definition.json @@ -0,0 +1,76 @@ +{ + "family": "orchestrator-service", + "networkMode": "awsvpc", + "containerDefinitions": [ + { + "name": "postgres", + "image": "postgres:13", + "essential": true, + "environment": [ + { "name": "POSTGRES_USER", "value": "myuser" }, + { "name": "POSTGRES_PASSWORD", "value": "mypassword" }, + { "name": "POSTGRES_DB", "value": "mydatabase" } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "ecs-orchestrator-service", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "postgres" + } + }, + "healthCheck": { + "command": [ + "CMD-SHELL", + "pg_isready -U myuser -d mydatabase" + ], + "interval": 10, + "timeout": 5, + "retries": 5 + }, + "mountPoints": [ + { + "sourceVolume": "pgdata", + "containerPath": "/var/lib/postgresql/data" + } + ] + }, + { + "name": "orchestrator", + "image": "satoshispalace/orchestrator:latest", + "essential": true, + "environment": [ + { "name": "DB_HOST", "value": "postgres" }, + { "name": "DB_PORT", "value": "5432" }, + { "name": "DB_USER", "value": "myuser" }, + { "name": "DB_PASSWORD", "value": "mypassword" }, + { "name": "DB_NAME", "value": "mydatabase" } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "ecs-orchestrator-service", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "orchestrator" + } + }, + "mountPoints": [] + } + ], + "volumes": [ + { + "name": "pgdata", + "efsVolumeConfiguration": { + "fileSystemId": "fs-01a952c26605adac6", + "rootDirectory": "/" + } + } + ], + "requiresCompatibilities": [ + "FARGATE" + ], + "cpu": "512", + "memory": "1024", + "executionRoleArn": "arn:aws:iam::615299754404:role/orchestrator-service-execution-role", + "taskRoleArn": "arn:aws:iam::615299754404:role/orchestrator-service-task-role" +} diff --git a/task-role-policy.json b/task-role-policy.json new file mode 100644 index 0000000..b833d12 --- /dev/null +++ b/task-role-policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} diff --git a/verifiable-delay-function/Dockerfile b/verifiable-delay-function/Dockerfile new file mode 100644 index 0000000..1064f5f --- /dev/null +++ b/verifiable-delay-function/Dockerfile @@ -0,0 +1,35 @@ +# Use an official Python image as a base +FROM python:3.8-slim + +# Install system dependencies needed for gmpy2 and PostgreSQL connection +RUN apt-get update && apt-get install -y \ + libgmp-dev \ + libmpfr-dev \ + libmpc-dev \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Set up a working directory +WORKDIR /app + +# Copy only the requirements file to leverage Docker cache +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code +COPY . . + +# Set environment variables for PostgreSQL credentials (can be overridden at runtime) +ENV DB_NAME=mydatabase \ + DB_USER=myuser \ + DB_PASSWORD=mypassword \ + DB_HOST=localhost \ + DB_PORT=5432 + +# Expose any necessary ports (optional, specify if your app uses specific ports) +# EXPOSE 8000 + +# Command to run the main script +# CMD ["python", "main.py"] diff --git a/verifiable-delay-function/main.py b/verifiable-delay-function/main.py index 6d061be..f7a4762 100644 --- a/verifiable-delay-function/main.py +++ b/verifiable-delay-function/main.py @@ -21,16 +21,16 @@ def main(): print("VDF output (y):", y) print(f"Proof generation time: {generation_time:.4f} seconds") - # Time the parallel verification - start_time = time.time() - is_valid_parallel = vdf.parallel_verify(y, proof) - parallel_verification_time = time.time() - start_time - print("Parallel verification:", "Valid" if is_valid_parallel else "Invalid") - print(f"Parallel verification time: {parallel_verification_time:.4f} seconds") + # # Time the parallel verification + # start_time = time.time() + # is_valid_parallel = vdf.parallel_verify(y, proof) + # parallel_verification_time = time.time() - start_time + # print("Parallel verification:", "Valid" if is_valid_parallel else "Invalid") + # print(f"Parallel verification time: {parallel_verification_time:.4f} seconds") - if not is_valid_parallel: - print("Verification failed. Aborting save.") - return + # if not is_valid_parallel: + # print("Verification failed. Aborting save.") + # return # Convert the puzzle instance to a VerifiableDelayFunctionEntity for database storage entity: VerifiableDelayFunctionEntity = conver_verifiable_delay_function_to_entity(vdf) diff --git a/verifiable-delay-function/src/converters/verifiable_delay_function_converter.py b/verifiable-delay-function/src/converters/verifiable_delay_function_converter.py index 3a378fa..399eba6 100644 --- a/verifiable-delay-function/src/converters/verifiable_delay_function_converter.py +++ b/verifiable-delay-function/src/converters/verifiable_delay_function_converter.py @@ -1,4 +1,5 @@ from typing import List +from gmpy2 import mpz from src.database.entity.verifiable_delay_function_entity import VerifiableDelayFunctionEntity from src.verifiable_delay_function.verifiable_delay_function import VerifiableDelayFunction From cbfff849c4261dc96f31f5ac144c720caa3011b6 Mon Sep 17 00:00:00 2001 From: ethan Date: Mon, 25 Nov 2024 12:29:52 -0500 Subject: [PATCH 02/80] added new requester --- .gitignore | 8 +- docker-compose.yml | 19 +- orchestrator/.dockerignore | 3 + orchestrator/Dockerfile | 2 +- orchestrator/README.md | 4 +- orchestrator/package.json | 5 +- orchestrator/src/app.ts | 52 +++- orchestrator/src/test.ts | 51 ++++ requester/.dockerignore | 3 + requester/Dockerfile | 23 ++ requester/docs/development.md | 0 requester/orchestrator/.dockerignore | 3 + requester/orchestrator/Dockerfile | 23 ++ requester/orchestrator/README.md | 6 + requester/orchestrator/docs/development.md | 0 requester/orchestrator/package.json | 27 ++ requester/orchestrator/src/app.ts | 313 +++++++++++++++++++++ requester/orchestrator/src/test.ts | 51 ++++ requester/orchestrator/tsconfig.json | 16 ++ requester/package.json | 27 ++ requester/src/app.ts | 96 +++++++ requester/tsconfig.json | 16 ++ terraform/backend.tf | 0 terraform/debug_task.tf | 36 +++ terraform/ecs_cluster.tf | 16 ++ terraform/ecs_service.tf | 35 +++ terraform/iam_roles.tf | 80 ++++++ terraform/logging.tf | 6 + terraform/networking.tf | 30 ++ terraform/outputs.tf | 5 + terraform/package.json | 8 + terraform/posgress.tf | 24 ++ terraform/providers.tf | 12 + terraform/task_definitions.tf | 73 +++++ terraform/variables.tf | 25 ++ 35 files changed, 1082 insertions(+), 16 deletions(-) create mode 100644 orchestrator/.dockerignore create mode 100644 orchestrator/src/test.ts create mode 100644 requester/.dockerignore create mode 100644 requester/Dockerfile create mode 100644 requester/docs/development.md create mode 100644 requester/orchestrator/.dockerignore create mode 100644 requester/orchestrator/Dockerfile create mode 100644 requester/orchestrator/README.md create mode 100644 requester/orchestrator/docs/development.md create mode 100644 requester/orchestrator/package.json create mode 100644 requester/orchestrator/src/app.ts create mode 100644 requester/orchestrator/src/test.ts create mode 100644 requester/orchestrator/tsconfig.json create mode 100644 requester/package.json create mode 100644 requester/src/app.ts create mode 100644 requester/tsconfig.json create mode 100644 terraform/backend.tf create mode 100644 terraform/debug_task.tf create mode 100644 terraform/ecs_cluster.tf create mode 100644 terraform/ecs_service.tf create mode 100644 terraform/iam_roles.tf create mode 100644 terraform/logging.tf create mode 100644 terraform/networking.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/package.json create mode 100644 terraform/posgress.tf create mode 100644 terraform/providers.tf create mode 100644 terraform/task_definitions.tf create mode 100644 terraform/variables.tf diff --git a/.gitignore b/.gitignore index 77d110f..16af4e4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ venv/ .env/ .env env +wallet.json # Ignore distribution/build directories dist/ @@ -10,16 +11,19 @@ build/ *.egg-info/ *.pyc __pycache__/ +pgdata/ # Ignore Node.js dependencies node_modules/ **/node_modules/ # Ignore Terraform and related files -terraform/ -**/.terraform/ *.tfstate *.tfstate.backup +*.exe +*.lock.* +LICENSE.txt + # Ignore package locks and dependency files package-lock.json diff --git a/docker-compose.yml b/docker-compose.yml index 1341f34..9a289c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.1.3 + image: randao/orchestrator:v0.1.8 depends_on: postgres: condition: service_healthy @@ -31,18 +31,33 @@ services: DB_PASSWORD: ${DB_PASSWORD:-mypassword} DB_NAME: ${DB_NAME:-mydatabase} ENVIRONMENT: local + PATH_TO_WALLET: /app/wallet.json # Path inside the container + WALLET_JSON: ${WALLET_JSON} + PROVIDER_ID: ${PROVIDER_ID} DOCKER_NETWORK: backend # Passing the network name networks: - backend volumes: - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket + - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + + requester: + image: randao/requester:v0.1.1 + environment: + PATH_TO_WALLET: /app/wallet.json # Path inside the container + REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} + PROVIDER_ID: ${PROVIDER_ID} + DOCKER_NETWORK: backend # Passing the network name + networks: + - backend + volumes: + - ./wallet.json:/app/wallet.json # Mount wallet.json into the container networks: backend: name: backend # This will set the network name explicitly driver: bridge - volumes: pgdata: driver: local diff --git a/orchestrator/.dockerignore b/orchestrator/.dockerignore new file mode 100644 index 0000000..a0bedda --- /dev/null +++ b/orchestrator/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.git +dist diff --git a/orchestrator/Dockerfile b/orchestrator/Dockerfile index 5c17fc9..e0b8740 100644 --- a/orchestrator/Dockerfile +++ b/orchestrator/Dockerfile @@ -1,5 +1,5 @@ # Use the official Node.js image as the base image -FROM node:16 +FROM node:20 # Create and set the working directory WORKDIR /usr/src/app diff --git a/orchestrator/README.md b/orchestrator/README.md index 14ca0d2..5470a55 100644 --- a/orchestrator/README.md +++ b/orchestrator/README.md @@ -1,6 +1,6 @@ cd into repo docker login -docker build -t satoshispalace/orchestrator:latest -t satoshispalace/orchestrator:v0.0.30 . -docker push satoshispalace/orchestrator:v0.0.30 +docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.1.4 . +docker push satoshispalace/orchestrator:v0.1.4 docker push satoshispalace/orchestrator:latest diff --git a/orchestrator/package.json b/orchestrator/package.json index 19385c4..01f8b49 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -1,12 +1,13 @@ { "devDependencies": { "@types/dockerode": "^3.3.31", - "@types/node": "^22.9.0", + "@types/node": "^22.9.1", "@types/pg": "^8.11.10", "serverless-offline": "^14.3.3", "typescript": "^5.6.3" }, "dependencies": { + "ao-process-clients": "^2.3.2", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", @@ -23,4 +24,4 @@ "keywords": [], "author": "", "license": "ISC" -} +} \ No newline at end of file diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 0e53d17..b0d65c0 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,6 +1,17 @@ import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; +import { getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" + +// const RANDOM_CONFIG: RandomClientConfig = { +// tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, +// processId: getRandomClientAutoConfiguration().processId, +// wallet: JSON.parse(process.env.WALLET_JSON!), +// environment: 'mainnet' +// } +const randclient: IRandomClient = RandomClient.autoConfiguration() +//const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG) + const docker = new Docker(); // Database configuration @@ -13,7 +24,7 @@ const dbConfig = { }; // Constants for configuration -const POLLING_INTERVAL_MS = 100; +const POLLING_INTERVAL_MS = 500; const MINIMUM_ENTRIES = 50; const TARGET_ENTRIES = 75; const DROP_CHANCE = 0.005; @@ -29,6 +40,9 @@ const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks const ongoingContainers = new Set(); // Track container IDs of running Docker containers +const PROVIDER_ID = process.env.PROVIDER_ID || "0"; + + let ongoingRequest = false; @@ -211,7 +225,9 @@ async function checkAndFetchIfNeeded(client: Client): Promise { const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); const currentCount = parseInt(res.rows[0].count, 10); - + const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(currentCount); + console.log("Updates onchain: " + updateAvailableValuesResult) + console.log(await randclient.getProviderAvailableValues(PROVIDER_ID)) if (currentCount < MINIMUM_ENTRIES) { const entriesNeeded = TARGET_ENTRIES - currentCount; console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries to reach ${TARGET_ENTRIES}...`); @@ -241,6 +257,27 @@ async function checkAndFetchIfNeeded(client: Client): Promise { // Polling function to manage entries and delete old ones occasionally async function polling(client: Client): Promise { + await checkAndFetchIfNeeded(client); + + try { + console.log(PROVIDER_ID); + + const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + console.log(openRequests); + + if (openRequests && openRequests.activeRequests && openRequests.activeRequests.request_ids) { + console.log(openRequests.providerId); + console.log(openRequests.activeRequests); + console.log(openRequests.activeRequests.request_ids); + console.log(openRequests.activeRequests.request_ids.length); + } else { + console.log('No requests'); + } + } catch (error) { + console.error('An error occurred while fetching open random requests:', error); + } + + if (Math.random() < DROP_CHANCE) { try { console.log("Randomly chosen to log and delete the oldest entry..."); @@ -258,7 +295,6 @@ async function polling(client: Client): Promise { console.error('Error logging and deleting oldest entry:', error); } } else { - await checkAndFetchIfNeeded(client); } } @@ -267,15 +303,17 @@ async function run(): Promise { const client = await connectWithRetry(); await setupDatabase(client); - setInterval(async () => { - await polling(client); - }, POLLING_INTERVAL_MS); - setInterval(async () => { const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); console.log(`Periodic log - Current database size: ${res.rows[0].count}`); }, 10000); + setInterval(async () => { + await polling(client); + }, POLLING_INTERVAL_MS); + + + process.on("SIGTERM", async () => { console.log("SIGTERM received. Closing database connection."); await client.end(); diff --git a/orchestrator/src/test.ts b/orchestrator/src/test.ts new file mode 100644 index 0000000..2b56b81 --- /dev/null +++ b/orchestrator/src/test.ts @@ -0,0 +1,51 @@ +import { IRandomClient, RandomClient } from "ao-process-clients"; + +const PROVIDER_ID = process.env.PROVIDER_ID || "0"; + +async function main() { + try { + const randclient: IRandomClient = RandomClient.autoConfiguration(); + + console.log("Testing `createRequest`..."); + const createRequestResult = await randclient.createRequest(["provider1", "provider2"]); + console.log("createRequest result:", createRequestResult); + + console.log("Testing `getOpenRandomRequests`..."); + const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + console.log("getOpenRandomRequests result:", openRequests); + + console.log("Testing `getProviderAvailableValues`..."); + const availableValues = await randclient.getProviderAvailableValues(PROVIDER_ID); + console.log("getProviderAvailableValues result:", availableValues); + + console.log("Testing `getRandomRequests`..."); + const randomRequests = await randclient.getRandomRequests(["request1", "request2"]); + console.log("getRandomRequests result:", randomRequests); + + console.log("Testing `postVDFChallenge`..."); + const postVDFChallengeResult = await randclient.postVDFChallenge( + "request1", + "modulus_value", + "input_value" + ); + console.log("postVDFChallenge result:", postVDFChallengeResult); + + console.log("Testing `postVDFOutputAndProof`..."); + const postVDFOutputAndProofResult = await randclient.postVDFOutputAndProof( + "request1", + "output_value", + "proof_value" + ); + console.log("postVDFOutputAndProof result:", postVDFOutputAndProofResult); + + console.log("Testing `updateProviderAvailableValues`..."); + const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(42); + console.log("updateProviderAvailableValues result:", updateAvailableValuesResult); + + } catch (error) { + console.error("An error occurred during testing:", error); + } +} + +// Call the main function +main(); diff --git a/requester/.dockerignore b/requester/.dockerignore new file mode 100644 index 0000000..a0bedda --- /dev/null +++ b/requester/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.git +dist diff --git a/requester/Dockerfile b/requester/Dockerfile new file mode 100644 index 0000000..e0b8740 --- /dev/null +++ b/requester/Dockerfile @@ -0,0 +1,23 @@ +# Use the official Node.js image as the base image +FROM node:20 + +# Create and set the working directory +WORKDIR /usr/src/app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the entire project into the container +COPY . . + +# Compile TypeScript to JavaScript +RUN npx tsc + +# Expose the port if needed +EXPOSE 3000 + +# Run the compiled app +CMD ["node", "dist/app.js"] diff --git a/requester/docs/development.md b/requester/docs/development.md new file mode 100644 index 0000000..e69de29 diff --git a/requester/orchestrator/.dockerignore b/requester/orchestrator/.dockerignore new file mode 100644 index 0000000..a0bedda --- /dev/null +++ b/requester/orchestrator/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.git +dist diff --git a/requester/orchestrator/Dockerfile b/requester/orchestrator/Dockerfile new file mode 100644 index 0000000..e0b8740 --- /dev/null +++ b/requester/orchestrator/Dockerfile @@ -0,0 +1,23 @@ +# Use the official Node.js image as the base image +FROM node:20 + +# Create and set the working directory +WORKDIR /usr/src/app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the entire project into the container +COPY . . + +# Compile TypeScript to JavaScript +RUN npx tsc + +# Expose the port if needed +EXPOSE 3000 + +# Run the compiled app +CMD ["node", "dist/app.js"] diff --git a/requester/orchestrator/README.md b/requester/orchestrator/README.md new file mode 100644 index 0000000..5470a55 --- /dev/null +++ b/requester/orchestrator/README.md @@ -0,0 +1,6 @@ +cd into repo + +docker login +docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.1.4 . +docker push satoshispalace/orchestrator:v0.1.4 +docker push satoshispalace/orchestrator:latest diff --git a/requester/orchestrator/docs/development.md b/requester/orchestrator/docs/development.md new file mode 100644 index 0000000..e69de29 diff --git a/requester/orchestrator/package.json b/requester/orchestrator/package.json new file mode 100644 index 0000000..01f8b49 --- /dev/null +++ b/requester/orchestrator/package.json @@ -0,0 +1,27 @@ +{ + "devDependencies": { + "@types/dockerode": "^3.3.31", + "@types/node": "^22.9.1", + "@types/pg": "^8.11.10", + "serverless-offline": "^14.3.3", + "typescript": "^5.6.3" + }, + "dependencies": { + "ao-process-clients": "^2.3.2", + "aws-sdk": "^2.1692.0", + "axios": "^1.7.7", + "crypto": "^1.0.1", + "dockerode": "^4.0.2", + "pg": "^8.13.1" + }, + "name": "ao-vrf", + "description": "1. To build:\r ```\r docker build -t serverless-multi-cloud .\r ```", + "version": "1.0.0", + "main": "Organizer.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/requester/orchestrator/src/app.ts b/requester/orchestrator/src/app.ts new file mode 100644 index 0000000..33c4cab --- /dev/null +++ b/requester/orchestrator/src/app.ts @@ -0,0 +1,313 @@ +import { Client } from 'pg'; +import Docker from 'dockerode'; +import AWS from 'aws-sdk'; +import { getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" + +// const RANDOM_CONFIG: RandomClientConfig = { +// tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, +// processId: getRandomClientAutoConfiguration().processId, +// wallet: JSON.parse(process.env.WALLET_JSON!), +// environment: 'mainnet' +// } +const randclient: IRandomClient = RandomClient.autoConfiguration() +//const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG) + +const docker = new Docker(); + +// Database configuration +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + user: process.env.DB_USER || 'myuser', + password: process.env.DB_PASSWORD || 'mypassword', + database: process.env.DB_NAME || 'mydatabase', +}; + +// Constants for configuration +const POLLING_INTERVAL_MS = 100; +const MINIMUM_ENTRIES = 50; +const TARGET_ENTRIES = 75; +const DROP_CHANCE = 0.005; +//Expected increments per second=10×0.005=0.05 +//180 times per hour +//4,320 times per day +//1,576,800 times per year +const MAX_OUTSTANDING_REQUESTS = 10; +const MAX_RETRIES = 10; +const RETRY_DELAY_MS = 10000; +const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.0'; +const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; +const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); +const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks +const ongoingContainers = new Set(); // Track container IDs of running Docker containers +const PROVIDER_ID = process.env.PROVIDER_ID || "0"; + + + +let ongoingRequest = false; + +// Retry logic for connecting to PostgreSQL +async function connectWithRetry(): Promise { + const client = new Client(dbConfig); + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + await client.connect(); + console.log(`Connected to PostgreSQL database (Attempt ${attempt})`); + return client; + } catch (error) { + console.error(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + } + } + throw new Error("Failed to connect to PostgreSQL after multiple attempts"); +} + +// Setup the `verifiable_delay_functions` table if not exists +async function setupDatabase(client: Client): Promise { + await client.query(` + CREATE TABLE IF NOT EXISTS verifiable_delay_functions ( + id TEXT PRIMARY KEY, -- Define as TEXT to match UUID format + request_id TEXT, + modulus TEXT NOT NULL, + input TEXT NOT NULL, + output TEXT NOT NULL, + proof JSON NOT NULL, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + console.log("Database setup complete. 'verifiable_delay_functions' table is ready."); +} + +// Added variable to track the number of Spot instance interruptions +let spotInterruptions = 0; + +// Modified function to trigger VDF job pod using ECS or Docker +async function triggerVDFJobPod(): Promise { + if (ENVIRONMENT === 'cloud') { + try { + console.log("Cloud environment detected. Launching ECS task."); + const result = await ecs.runTask({ + cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', + taskDefinition: 'vdf-job', + capacityProviderStrategy: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 1 + } + ], + networkConfiguration: { + awsvpcConfiguration: { + subnets: [process.env.SUBNET_ID || 'subnet-12345678'], + securityGroups: [process.env.SECURITY_GROUP || 'sg-12345678'], + assignPublicIp: 'ENABLED' + } + }, + overrides: { + containerOverrides: [ + { + name: 'vdf_job_container', + environment: [ + { name: 'DATABASE_TYPE', value: 'postgresql' }, + { name: 'DATABASE_HOST', value: process.env.DB_HOST || 'cloud-postgres-host' }, + { name: 'DATABASE_PORT', value: process.env.DB_PORT || '5432' }, + { name: 'DATABASE_USER', value: process.env.DB_USER || 'myuser' }, + { name: 'DATABASE_PASSWORD', value: process.env.DB_PASSWORD || 'mypassword' }, + { name: 'DATABASE_NAME', value: process.env.DB_NAME || 'mydatabase' }, + ] + } + ] + }, + count: 1 + }).promise(); + + const taskArn = result.tasks?.[0]?.taskArn; + if (taskArn) { + ongoingTasks.add(taskArn); + console.log(`ECS task started successfully: ${taskArn}`); + return taskArn; + } + return null; + } catch (error: any) { + if (error?.code === 'SpotCapacityNotAvailableException') { + spotInterruptions++; + console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); + } else { + console.error("Error launching ECS task:", error); + } + return null; + } + } else { + const containerName = `vdf_job_${Date.now()}`; + console.log(`Starting Docker container with name: ${containerName}`); + const container = await docker.createContainer({ + Image: VDF_JOB_IMAGE, + Cmd: ['python', 'main.py'], + Env: [ + `DATABASE_TYPE=postgresql`, + `DATABASE_HOST=${dbConfig.host}`, + `DATABASE_PORT=${dbConfig.port.toString()}`, + `DATABASE_USER=${dbConfig.user}`, + `DATABASE_PASSWORD=${dbConfig.password}`, + `DATABASE_NAME=${dbConfig.database}`, + ], + HostConfig: { + NetworkMode: 'backend', + }, + name: containerName + }); + + await container.start(); + ongoingContainers.add(container.id); + console.log(`Docker container ${containerName} started successfully.`); + return container.id; + } +} + +// Modified function to wait for ECS tasks to complete and remove them from tracking +async function monitorECSTasks(): Promise { + if (ongoingTasks.size === 0) return; + + const describeTasksResult = await ecs.describeTasks({ + cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', + tasks: Array.from(ongoingTasks) + }).promise(); + + describeTasksResult.tasks?.forEach(task => { + // Check capacity provider name to determine if it is running Fargate or Fargate Spot + const capacityProvider = task.capacityProviderName || 'Unknown'; + + console.log(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); + + if (task.lastStatus === 'STOPPED') { + console.log(`ECS task stopped: ${task.taskArn}`); + if (task.stoppedReason) { + console.log(`Task stopped reason: ${task.stoppedReason}`); + if (task.stoppedReason.includes('Host EC2 instance termination')) { + spotInterruptions++; + console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); + } + } else { + console.log('Task stopped due to normal completion.'); + } + ongoingTasks.delete(task.taskArn as string); + } + }); +} + + + + +// Function to wait for Docker containers to complete and remove them from tracking +async function monitorDockerContainers(): Promise { + if (ongoingContainers.size === 0) return; + + for (const containerId of ongoingContainers) { + try { + const container = docker.getContainer(containerId); + const containerInfo = await container.inspect(); + + if (containerInfo.State.Status === 'exited') { + console.log(`Docker container stopped: ${containerId}`); + await container.remove(); + ongoingContainers.delete(containerId); + } + } catch (error) { + console.error(`Error monitoring Docker container ${containerId}:`, error); + ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) + } + } +} + +// Function to check if there are fewer than MINIMUM_ENTRIES and fetch until TARGET_ENTRIES +async function checkAndFetchIfNeeded(client: Client): Promise { + try { + if (ongoingRequest) return; + + const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); + const currentCount = parseInt(res.rows[0].count, 10); + const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(currentCount); + console.log("Updates onchain: " + updateAvailableValuesResult) + console.log(await randclient.getProviderAvailableValues(PROVIDER_ID)) + if (currentCount < MINIMUM_ENTRIES) { + const entriesNeeded = TARGET_ENTRIES - currentCount; + console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries to reach ${TARGET_ENTRIES}...`); + ongoingRequest = true; + let tasksTriggered = 0; + + while (tasksTriggered < entriesNeeded) { + if (ENVIRONMENT === 'cloud' && ongoingTasks.size >= MAX_OUTSTANDING_REQUESTS) { + await monitorECSTasks(); + } else if (ENVIRONMENT !== 'cloud' && ongoingContainers.size >= MAX_OUTSTANDING_REQUESTS) { + await monitorDockerContainers(); + } else { + const taskArn = await triggerVDFJobPod(); + if (taskArn) { + tasksTriggered++; + console.log(`Task triggered: ${taskArn}. Total ongoing tasks: ${ongoingTasks.size}. Total ongoing containers ${+ ongoingContainers.size}`); + } + } + } + ongoingRequest = false; + } + } catch (error) { + console.error('Error during check and fetch:', error); + ongoingRequest = false; + } +} + +// Polling function to manage entries and delete old ones occasionally +async function polling(client: Client): Promise { + await checkAndFetchIfNeeded(client); + + console.log(PROVIDER_ID) + var openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID) + console.log(openRequests) + console.log(openRequests.providerId) + console.log(openRequests.activeRequests) + console.log(openRequests.activeRequests.request_ids) + console.log(openRequests.activeRequests.request_ids.length) + + if (Math.random() < DROP_CHANCE) { + try { + console.log("Randomly chosen to log and delete the oldest entry..."); + const res = await client.query('SELECT * FROM verifiable_delay_functions ORDER BY id ASC LIMIT 1'); + const entry = res.rows[0]; + + if (entry) { + console.log("Logging and deleting oldest entry:", JSON.stringify(entry, null, 2)); + await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [entry.id]); + console.log("Oldest entry deleted from database."); + } else { + console.log("No entries available to delete."); + } + } catch (error) { + console.error('Error logging and deleting oldest entry:', error); + } + } else { + } +} + +// Main function +async function run(): Promise { + const client = await connectWithRetry(); + await setupDatabase(client); + + setInterval(async () => { + const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); + console.log(`Periodic log - Current database size: ${res.rows[0].count}`); + }, 10000); + + setInterval(async () => { + await polling(client); + }, POLLING_INTERVAL_MS); + + + + process.on("SIGTERM", async () => { + console.log("SIGTERM received. Closing database connection."); + await client.end(); + process.exit(0); + }); +} + +run().catch((err) => console.error(`Error in main function: ${err}`)); diff --git a/requester/orchestrator/src/test.ts b/requester/orchestrator/src/test.ts new file mode 100644 index 0000000..2b56b81 --- /dev/null +++ b/requester/orchestrator/src/test.ts @@ -0,0 +1,51 @@ +import { IRandomClient, RandomClient } from "ao-process-clients"; + +const PROVIDER_ID = process.env.PROVIDER_ID || "0"; + +async function main() { + try { + const randclient: IRandomClient = RandomClient.autoConfiguration(); + + console.log("Testing `createRequest`..."); + const createRequestResult = await randclient.createRequest(["provider1", "provider2"]); + console.log("createRequest result:", createRequestResult); + + console.log("Testing `getOpenRandomRequests`..."); + const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + console.log("getOpenRandomRequests result:", openRequests); + + console.log("Testing `getProviderAvailableValues`..."); + const availableValues = await randclient.getProviderAvailableValues(PROVIDER_ID); + console.log("getProviderAvailableValues result:", availableValues); + + console.log("Testing `getRandomRequests`..."); + const randomRequests = await randclient.getRandomRequests(["request1", "request2"]); + console.log("getRandomRequests result:", randomRequests); + + console.log("Testing `postVDFChallenge`..."); + const postVDFChallengeResult = await randclient.postVDFChallenge( + "request1", + "modulus_value", + "input_value" + ); + console.log("postVDFChallenge result:", postVDFChallengeResult); + + console.log("Testing `postVDFOutputAndProof`..."); + const postVDFOutputAndProofResult = await randclient.postVDFOutputAndProof( + "request1", + "output_value", + "proof_value" + ); + console.log("postVDFOutputAndProof result:", postVDFOutputAndProofResult); + + console.log("Testing `updateProviderAvailableValues`..."); + const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(42); + console.log("updateProviderAvailableValues result:", updateAvailableValuesResult); + + } catch (error) { + console.error("An error occurred during testing:", error); + } +} + +// Call the main function +main(); diff --git a/requester/orchestrator/tsconfig.json b/requester/orchestrator/tsconfig.json new file mode 100644 index 0000000..493e143 --- /dev/null +++ b/requester/orchestrator/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "commonjs", + "target": "es6", + "strict": true, + "esModuleInterop": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/requester/package.json b/requester/package.json new file mode 100644 index 0000000..01f8b49 --- /dev/null +++ b/requester/package.json @@ -0,0 +1,27 @@ +{ + "devDependencies": { + "@types/dockerode": "^3.3.31", + "@types/node": "^22.9.1", + "@types/pg": "^8.11.10", + "serverless-offline": "^14.3.3", + "typescript": "^5.6.3" + }, + "dependencies": { + "ao-process-clients": "^2.3.2", + "aws-sdk": "^2.1692.0", + "axios": "^1.7.7", + "crypto": "^1.0.1", + "dockerode": "^4.0.2", + "pg": "^8.13.1" + }, + "name": "ao-vrf", + "description": "1. To build:\r ```\r docker build -t serverless-multi-cloud .\r ```", + "version": "1.0.0", + "main": "Organizer.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/requester/src/app.ts b/requester/src/app.ts new file mode 100644 index 0000000..5e609ca --- /dev/null +++ b/requester/src/app.ts @@ -0,0 +1,96 @@ +import { + getRandomClientAutoConfiguration, + IRandomClient, + RandomClient, + RandomClientConfig, +} from "ao-process-clients"; + +const PROVIDER_ID = "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE"; +const RETRY_DELAY_MS = 500; +const CHANCE_TO_CALL_RANDOM = 0.05; + +const RANDOM_CONFIG: RandomClientConfig = { + tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, + processId: getRandomClientAutoConfiguration().processId, + wallet: JSON.parse(process.env.REQUEST_WALLET_JSON!), + environment: "mainnet", +}; + +let totalRandomCalled = 0; +let totalTimeToFulfill = 0; +let fulfilledRequests = 0; +const outstandingRequests: Set = new Set(); + +async function main() { + const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG); + + while (true) { + try { + // Roll for random chance to make a request + if (Math.random() < CHANCE_TO_CALL_RANDOM) { + console.log("Initiating random request..."); + await randclient.createRequest([PROVIDER_ID]); + totalRandomCalled++; + console.log("Random request initiated. Awaiting request ID in open requests..."); + } + + // Check open requests + const openRequestsResponse = await randclient.getOpenRandomRequests(PROVIDER_ID); + const openRequestIds = openRequestsResponse.activeRequests.request_ids || []; + console.log("Open requests:", openRequestIds); + + // Track outstanding requests + for (const requestId of openRequestIds) { + if (!outstandingRequests.has(requestId)) { + console.log(`Tracking new request: ${requestId}`); + outstandingRequests.add(requestId); + } + } + + // Check the status of outstanding requests + if (outstandingRequests.size > 0) { + const randomRequestsResponse = await randclient.getRandomRequests(Array.from(outstandingRequests)); + const requests = randomRequestsResponse.randomRequestResponses || []; // Adjust based on actual response structure + console.log(randomRequestsResponse) + console.log(requests) + + // for (const request of requests) { + // const requestId = request.requestId; // Adjust if property has a different name + // if (request?.status === "fulfilled") { + // const fulfilledTime = Date.now(); + // const timeToFulfill = fulfilledTime - request.createdTime; // Adjust if createdTime exists + // totalTimeToFulfill += timeToFulfill; + // fulfilledRequests++; + // console.log(`Request ${requestId} fulfilled. Time to fulfill: ${timeToFulfill}ms`); + // outstandingRequests.delete(requestId); // Stop tracking fulfilled requests + // } else { + // console.log(`Request ${requestId} is still being processed.`); + // } + // } + } + + // Calculate and log stats + if (fulfilledRequests > 0) { + const avgTimeToFulfill = totalTimeToFulfill / fulfilledRequests; + + console.log(` + Total Random Called: ${totalRandomCalled} + Outstanding Requests: ${outstandingRequests.size} + Average Time to Fulfill: ${avgTimeToFulfill}ms + `); + } + + // Wait before next cycle + await delay(RETRY_DELAY_MS); + } catch (error) { + console.error("An error occurred:", error); + } + } +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// Call the main function +main(); diff --git a/requester/tsconfig.json b/requester/tsconfig.json new file mode 100644 index 0000000..493e143 --- /dev/null +++ b/requester/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "commonjs", + "target": "es6", + "strict": true, + "esModuleInterop": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 0000000..e69de29 diff --git a/terraform/debug_task.tf b/terraform/debug_task.tf new file mode 100644 index 0000000..9972781 --- /dev/null +++ b/terraform/debug_task.tf @@ -0,0 +1,36 @@ +# debug_task.tf + +resource "aws_ecs_task_definition" "debug_task" { + family = "debug-task" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = "512" + memory = "1024" + + container_definitions = jsonencode([ + { + name = "debug-container" + image = "amazonlinux" + essential = true + command = ["/bin/sh", "-c", "while true; do sleep 60; done"] + environment = [ + { name = "DB_HOST", value = "postgres" }, + { name = "DB_PORT", value = "5432" }, + { name = "DB_USER", value = "myuser" }, + { name = "DB_PASSWORD", value = "mypassword" }, + { name = "DB_NAME", value = "mydatabase" } + ], + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = "/ecs/debug-task" + awslogs-region = var.aws_region + awslogs-stream-prefix = "debug-container" + } + } + } + ]) + + execution_role_arn = aws_iam_role.execution_role.arn + task_role_arn = aws_iam_role.task_role.arn +} diff --git a/terraform/ecs_cluster.tf b/terraform/ecs_cluster.tf new file mode 100644 index 0000000..012ad04 --- /dev/null +++ b/terraform/ecs_cluster.tf @@ -0,0 +1,16 @@ +# ecs_cluster.tf +resource "aws_ecs_cluster" "fargate_cluster" { + name = "fargate-cluster" +} + +# Configure the Capacity Providers for the ECS Cluster +resource "aws_ecs_cluster_capacity_providers" "fargate_cluster_capacity" { + cluster_name = aws_ecs_cluster.fargate_cluster.name + + capacity_providers = ["FARGATE", "FARGATE_SPOT"] + + default_capacity_provider_strategy { + capacity_provider = "FARGATE_SPOT" + weight = 1 + } +} diff --git a/terraform/ecs_service.tf b/terraform/ecs_service.tf new file mode 100644 index 0000000..fb94e60 --- /dev/null +++ b/terraform/ecs_service.tf @@ -0,0 +1,35 @@ +# ecs_service.tf - Updated to Only Deploy Orchestrator Service + +resource "aws_ecs_service" "orchestrator_service" { + name = "orchestrator-service" + cluster = aws_ecs_cluster.fargate_cluster.id + task_definition = aws_ecs_task_definition.orchestrator_service.arn + desired_count = 1 + launch_type = "FARGATE" + + network_configuration { + subnets = local.subnet_ids + security_groups = [data.aws_security_group.default.id] + assign_public_ip = true + } +} + +resource "aws_ecs_service" "vdf_job_service" { + name = "vdf-job-service" + cluster = aws_ecs_cluster.fargate_cluster.id + task_definition = aws_ecs_task_definition.vdf_job.arn + desired_count = 0 + + capacity_provider_strategy { + capacity_provider = "FARGATE_SPOT" + weight = 1 + } + + network_configuration { + subnets = local.subnet_ids + security_groups = [data.aws_security_group.default.id] + assign_public_ip = true + } +} + + diff --git a/terraform/iam_roles.tf b/terraform/iam_roles.tf new file mode 100644 index 0000000..55b055c --- /dev/null +++ b/terraform/iam_roles.tf @@ -0,0 +1,80 @@ +# iam_roles.tf + +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "task_role" { + name = "orchestrator-task-role" + assume_role_policy = data.aws_iam_policy_document.task_role_policy.json +} + +resource "aws_iam_role" "execution_role" { + name = "orchestrator-execution-role" + assume_role_policy = data.aws_iam_policy_document.execution_role_policy.json +} + +data "aws_iam_policy_document" "task_role_policy" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "execution_role_policy" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +# Attach necessary policies to orchestrator-task-role +resource "aws_iam_role_policy" "orchestrator_task_policy" { + name = "orchestrator-task-policy" + role = aws_iam_role.task_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ecs:RunTask", + "ecs:DescribeTasks", + "ecs:DescribeTaskDefinition", + "ecs:ListTasks" + ] + Resource = [ + "arn:aws:ecs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:task-definition/vdf-job:*", + "arn:aws:ecs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:task/${aws_ecs_cluster.fargate_cluster.name}/*" + ] + }, + { + Effect = "Allow" + Action = [ + "iam:PassRole" + ] + Resource = [ + aws_iam_role.execution_role.arn, + aws_iam_role.task_role.arn + ] + } + ] + }) +} + + +# Attach policies to the execution role +resource "aws_iam_role_policy_attachment" "ecs_task_execution" { + role = aws_iam_role.execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role_policy_attachment" "ecs_logs_policy" { + role = aws_iam_role.execution_role.name + policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" +} diff --git a/terraform/logging.tf b/terraform/logging.tf new file mode 100644 index 0000000..5ee8731 --- /dev/null +++ b/terraform/logging.tf @@ -0,0 +1,6 @@ +# logging.tf + +resource "aws_cloudwatch_log_group" "ecs_orchestrator_service" { + name = "ecs-orchestrator-service" + retention_in_days = 30 # Adjust retention as needed +} diff --git a/terraform/networking.tf b/terraform/networking.tf new file mode 100644 index 0000000..f0f9919 --- /dev/null +++ b/terraform/networking.tf @@ -0,0 +1,30 @@ +# Get the default VPC +data "aws_vpc" "default" { + default = true +} + +# Fetch all subnets within the default VPC +data "aws_subnets" "default_vpc_subnets" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } +} + +# Use a local variable to store the subnet IDs +locals { + subnet_ids = data.aws_subnets.default_vpc_subnets.ids +} + +# Get the default security group within the VPC +data "aws_security_group" "default" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } + + filter { + name = "group-name" + values = ["default"] + } +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..b094fab --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,5 @@ +# outputs.tf + +output "postgres_endpoint" { + value = aws_db_instance.postgres.address +} diff --git a/terraform/package.json b/terraform/package.json new file mode 100644 index 0000000..9f6139e --- /dev/null +++ b/terraform/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "ao-process-clients": "^2.3.1" + }, + "devDependencies": { + "@types/node": "^22.9.1" + } +} diff --git a/terraform/posgress.tf b/terraform/posgress.tf new file mode 100644 index 0000000..c7d3ba4 --- /dev/null +++ b/terraform/posgress.tf @@ -0,0 +1,24 @@ +# posgress.tf - Corrected to use db_name for PostgreSQL in AWS RDS + +resource "aws_db_instance" "postgres" { + allocated_storage = 20 + storage_type = "gp2" + engine = "postgres" + engine_version = "13" + instance_class = "db.t3.micro" + db_name = var.db_name # Corrected argument + username = var.db_user + password = var.db_password + skip_final_snapshot = true + publicly_accessible = true # Ensure this matches your security needs + db_subnet_group_name = aws_db_subnet_group.default.name + vpc_security_group_ids = [data.aws_security_group.default.id] +} + +resource "aws_db_subnet_group" "default" { + name = "postgres-subnet-group" + subnet_ids = local.subnet_ids + tags = { + Name = "postgres-subnet-group" + } +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..fad7242 --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.0" + } + } +} + +provider "aws" { + region = "us-east-1" # or your specified region +} diff --git a/terraform/task_definitions.tf b/terraform/task_definitions.tf new file mode 100644 index 0000000..7755465 --- /dev/null +++ b/terraform/task_definitions.tf @@ -0,0 +1,73 @@ +# task_definitions.tf - Updated to Add VDF Job Task and Connect Orchestrator to RDS + +resource "aws_ecs_task_definition" "orchestrator_service" { + family = "orchestrator-service" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = "256" + memory = "512" + +container_definitions = jsonencode([{ + name = "orchestrator" + image = "randao/orchestrator:v0.1.4" + essential = true + environment = [ + { name = "ENVIRONMENT", value = "cloud" }, + { name = "DB_HOST", value = aws_db_instance.postgres.address }, + { name = "DB_PORT", value = "5432" }, + { name = "DB_USER", value = var.db_user }, + { name = "DB_PASSWORD", value = var.db_password }, + { name = "DB_NAME", value = var.db_name }, + { name = "ECS_CLUSTER_NAME", value = aws_ecs_cluster.fargate_cluster.name }, + { name = "SUBNET_ID", value = local.subnet_ids[0] }, # First subnet from your VPC + { name = "SECURITY_GROUP", value = data.aws_security_group.default.id } # Default security group + ] + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = aws_cloudwatch_log_group.ecs_orchestrator_service.name + awslogs-region = var.aws_region + awslogs-stream-prefix = "orchestrator" + } + } +}]) + + + + execution_role_arn = aws_iam_role.execution_role.arn + task_role_arn = aws_iam_role.task_role.arn +} + +resource "aws_ecs_task_definition" "vdf_job" { + family = "vdf-job" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = "256" + memory = "512" + +container_definitions = jsonencode([{ + name = "vdf_job_container" + image = "randao/vdf_job:v0.1.0" + essential = true + command = ["python", "main.py"] # Set the default command to run + environment = [ + { name = "DB_HOST", value = aws_db_instance.postgres.address }, + { name = "DB_PORT", value = "5432" }, + { name = "DB_USER", value = var.db_user }, + { name = "DB_PASSWORD", value = var.db_password }, + { name = "DB_NAME", value = var.db_name } + ] + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = aws_cloudwatch_log_group.ecs_orchestrator_service.name + awslogs-region = var.aws_region + awslogs-stream-prefix = "vdf-job" + } + } +}]) + + + execution_role_arn = aws_iam_role.execution_role.arn + task_role_arn = aws_iam_role.task_role.arn +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..690fe21 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,25 @@ +# variables.tf + +variable "aws_region" { + description = "The AWS region to deploy resources in" + type = string + default = "us-east-1" +} + +variable "db_user" { + description = "The PostgreSQL username" + type = string + default = "myuser" +} + +variable "db_password" { + description = "The PostgreSQL password" + type = string + default = "mypassword" +} + +variable "db_name" { + description = "The PostgreSQL database name" + type = string + default = "mydatabase" +} From acf7e68d640215a791b8cdfd71c7388dc7720fce Mon Sep 17 00:00:00 2001 From: ethan Date: Mon, 25 Nov 2024 13:41:31 -0500 Subject: [PATCH 03/80] added new requester --- docker-compose.yml | 4 ++-- orchestrator/src/app.ts | 3 ++- requester/src/app.ts | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9a289c0..b724817 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.1.8 + image: randao/orchestrator:v0.1.10 depends_on: postgres: condition: service_healthy @@ -42,7 +42,7 @@ services: - ./wallet.json:/app/wallet.json # Mount wallet.json into the container requester: - image: randao/requester:v0.1.1 + image: randao/requester:v0.1.5 environment: PATH_TO_WALLET: /app/wallet.json # Path inside the container REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index b0d65c0..4c39dfa 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -24,7 +24,7 @@ const dbConfig = { }; // Constants for configuration -const POLLING_INTERVAL_MS = 500; +const POLLING_INTERVAL_MS = 5000; const MINIMUM_ENTRIES = 50; const TARGET_ENTRIES = 75; const DROP_CHANCE = 0.005; @@ -263,6 +263,7 @@ async function polling(client: Client): Promise { console.log(PROVIDER_ID); const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + console.log("here") console.log(openRequests); if (openRequests && openRequests.activeRequests && openRequests.activeRequests.request_ids) { diff --git a/requester/src/app.ts b/requester/src/app.ts index 5e609ca..1e98b18 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -6,8 +6,8 @@ import { } from "ao-process-clients"; const PROVIDER_ID = "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE"; -const RETRY_DELAY_MS = 500; -const CHANCE_TO_CALL_RANDOM = 0.05; +const RETRY_DELAY_MS = 5000; +const CHANCE_TO_CALL_RANDOM = 1; const RANDOM_CONFIG: RandomClientConfig = { tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, @@ -25,6 +25,7 @@ async function main() { const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG); while (true) { + console.log("Running") try { // Roll for random chance to make a request if (Math.random() < CHANCE_TO_CALL_RANDOM) { @@ -93,4 +94,4 @@ function delay(ms: number): Promise { } // Call the main function -main(); +main(); \ No newline at end of file From 16790cce31410f6b18e51de1175257ff55257dc3 Mon Sep 17 00:00:00 2001 From: ethan Date: Mon, 25 Nov 2024 15:57:40 -0500 Subject: [PATCH 04/80] added new requester --- orchestrator/docs/development.md | 5 +++++ requester/docs/development.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index e69de29..e1a2d70 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -0,0 +1,5 @@ +To build: + +Save all files +Run: +docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.1.10 . \ No newline at end of file diff --git a/requester/docs/development.md b/requester/docs/development.md index e69de29..5cb89cb 100644 --- a/requester/docs/development.md +++ b/requester/docs/development.md @@ -0,0 +1,5 @@ +To build: + +Save all files +Run: +docker build -t randao/requester:latest -t randao/requester:v0.1.5 . \ No newline at end of file From da094d967956268c83596636c8eab7423af830a6 Mon Sep 17 00:00:00 2001 From: ethan Date: Mon, 25 Nov 2024 16:02:20 -0500 Subject: [PATCH 05/80] added new requester --- .env.example | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9d969ba --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +DB_USER=myuser +DB_PASSWORD=mypassword +DB_NAME=mydatabase +DOCKER_NETWORK=backend +PATH_TO_WALLET="wallet.json" +PROVIDER_ID = "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE" +WALLET_JSON = '{}' +REQUEST_WALLET_JSON = '{}' \ No newline at end of file From 0a23de433ee0886ae6531333ab506391a7306590 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 25 Nov 2024 18:17:08 -0500 Subject: [PATCH 06/80] orch now fuffils requests --- docker-compose.yml | 24 +++++------ orchestrator/src/app.ts | 91 ++++++++++++++++++++++++++--------------- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b724817..322fe19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.1.10 + image: randao/orchestrator:v0.1.16 depends_on: postgres: condition: service_healthy @@ -41,17 +41,17 @@ services: - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket - ./wallet.json:/app/wallet.json # Mount wallet.json into the container - requester: - image: randao/requester:v0.1.5 - environment: - PATH_TO_WALLET: /app/wallet.json # Path inside the container - REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - PROVIDER_ID: ${PROVIDER_ID} - DOCKER_NETWORK: backend # Passing the network name - networks: - - backend - volumes: - - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + # requester: + # image: randao/requester:v0.1.5 + # environment: + # PATH_TO_WALLET: /app/wallet.json # Path inside the container + # REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} + # PROVIDER_ID: ${PROVIDER_ID} + # DOCKER_NETWORK: backend # Passing the network name + # networks: + # - backend + # volumes: + # - ./wallet.json:/app/wallet.json # Mount wallet.json into the container networks: backend: diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 4c39dfa..b80e888 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -28,11 +28,12 @@ const POLLING_INTERVAL_MS = 5000; const MINIMUM_ENTRIES = 50; const TARGET_ENTRIES = 75; const DROP_CHANCE = 0.005; -//Expected increments per second=10×0.005=0.05 +//Expected increments per second=10×0.005=0.05 //180 times per hour //4,320 times per day //1,576,800 times per year const MAX_OUTSTANDING_REQUESTS = 10; +const MAX_OUTSTANDING_FULFILLMENTS = 10; const MAX_RETRIES = 10; const RETRY_DELAY_MS = 10000; const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.0'; @@ -40,11 +41,10 @@ const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks const ongoingContainers = new Set(); // Track container IDs of running Docker containers +const ongoingFulfillments = new Set(); // Track request IDs for ongoing fulfillments const PROVIDER_ID = process.env.PROVIDER_ID || "0"; - - - let ongoingRequest = false; +let spotInterruptions = 0; // Retry logic for connecting to PostgreSQL async function connectWithRetry(): Promise { @@ -78,8 +78,6 @@ async function setupDatabase(client: Client): Promise { console.log("Database setup complete. 'verifiable_delay_functions' table is ready."); } -// Added variable to track the number of Spot instance interruptions -let spotInterruptions = 0; // Modified function to trigger VDF job pod using ECS or Docker async function triggerVDFJobPod(): Promise { @@ -195,8 +193,6 @@ async function monitorECSTasks(): Promise { } - - // Function to wait for Docker containers to complete and remove them from tracking async function monitorDockerContainers(): Promise { if (ongoingContainers.size === 0) return; @@ -254,8 +250,43 @@ async function checkAndFetchIfNeeded(client: Client): Promise { ongoingRequest = false; } } +// Function to post VDF challenge and proof +async function fulfillRandomRequest(client: Client, dbId: string, requestId: string, modulus: string, input: string): Promise { + if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) return; + ongoingFulfillments.add(dbId); + try { + console.log(`Posting VDF challenge for entry ID: ${dbId}, request ID: ${requestId}`); + await randclient.postVDFChallenge(requestId, modulus, input); + console.log(`Challenge posted for request ID: ${requestId}. Waiting to post proof...`); + + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Fetch the output and proof from the database + const res = await client.query('SELECT output, proof FROM verifiable_delay_functions WHERE id = $1', [dbId]); + if (!res.rowCount) { + console.error(`No entry found for ID: ${dbId}`); + return; + } + const { output, proof } = res.rows[0]; -// Polling function to manage entries and delete old ones occasionally + // Stringify the proof if it is an array + const proofString = Array.isArray(proof) ? JSON.stringify(proof) : proof; + + console.log(`Posting VDF output and proof for request ID: ${requestId}`); + await randclient.postVDFOutputAndProof(requestId, output, proofString); + console.log(`Proof posted for request ID: ${requestId}`); + + // Delete the entry from the database after successfully posting the proof + await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [dbId]); + console.log(`Entry with ID: ${dbId} deleted from database.`); + } catch (error) { + console.error(`Error fulfilling random request for entry ID: ${dbId}, request ID: ${requestId}:`, error); + } finally { + ongoingFulfillments.delete(dbId); + } +} + +// Modified polling function to fulfill open requests if they exist async function polling(client: Client): Promise { await checkAndFetchIfNeeded(client); @@ -263,7 +294,7 @@ async function polling(client: Client): Promise { console.log(PROVIDER_ID); const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - console.log("here") + console.log("here"); console.log(openRequests); if (openRequests && openRequests.activeRequests && openRequests.activeRequests.request_ids) { @@ -271,34 +302,32 @@ async function polling(client: Client): Promise { console.log(openRequests.activeRequests); console.log(openRequests.activeRequests.request_ids); console.log(openRequests.activeRequests.request_ids.length); - } else { - console.log('No requests'); - } - } catch (error) { - console.error('An error occurred while fetching open random requests:', error); - } + // Check if there are enough entries in the database to fulfill the requests + const res = await client.query('SELECT * FROM verifiable_delay_functions ORDER BY id ASC LIMIT $1', [openRequests.activeRequests.request_ids.length]); - if (Math.random() < DROP_CHANCE) { - try { - console.log("Randomly chosen to log and delete the oldest entry..."); - const res = await client.query('SELECT * FROM verifiable_delay_functions ORDER BY id ASC LIMIT 1'); - const entry = res.rows[0]; - - if (entry) { - console.log("Logging and deleting oldest entry:", JSON.stringify(entry, null, 2)); - await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [entry.id]); - console.log("Oldest entry deleted from database."); + if (res.rowCount !== null && res.rowCount > 0) { + const rowsToProcess = Math.min(res.rowCount, openRequests.activeRequests.request_ids.length); + + for (let i = 0; i < rowsToProcess; i++) { + if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) break; + + const { id, modulus, input } = res.rows[i]; + const requestId = openRequests.activeRequests.request_ids[i]; + fulfillRandomRequest(client, id, requestId, modulus, input); + } } else { - console.log("No entries available to delete."); + console.log('Not enough entries in the database to fulfill all requests.'); } - } catch (error) { - console.error('Error logging and deleting oldest entry:', error); + } else { + console.log('No requests'); } - } else { + } catch (error) { + console.error('An error occurred while fetching open random requests:', error); } } + // Main function async function run(): Promise { const client = await connectWithRetry(); @@ -313,8 +342,6 @@ async function run(): Promise { await polling(client); }, POLLING_INTERVAL_MS); - - process.on("SIGTERM", async () => { console.log("SIGTERM received. Closing database connection."); await client.end(); From 100f34976c48d4546e8a861dd6beb453e85bf9ba Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 8 Dec 2024 16:57:24 -0500 Subject: [PATCH 07/80] changed loops --- docker-compose.yml | 4 +- orchestrator/package.json | 4 +- orchestrator/src/app.ts | 164 ++++++--- orchestrator/src/test.ts | 51 --- requester/orchestrator/.dockerignore | 3 - requester/orchestrator/Dockerfile | 23 -- requester/orchestrator/README.md | 6 - requester/orchestrator/docs/development.md | 0 requester/orchestrator/package.json | 27 -- requester/orchestrator/src/app.ts | 313 ------------------ requester/orchestrator/src/test.ts | 51 --- requester/orchestrator/tsconfig.json | 16 - requester/package.json | 2 +- requester/src/app.ts | 83 ++--- .../src/protocol_constants.py | 2 +- 15 files changed, 173 insertions(+), 576 deletions(-) delete mode 100644 orchestrator/src/test.ts delete mode 100644 requester/orchestrator/.dockerignore delete mode 100644 requester/orchestrator/Dockerfile delete mode 100644 requester/orchestrator/README.md delete mode 100644 requester/orchestrator/docs/development.md delete mode 100644 requester/orchestrator/package.json delete mode 100644 requester/orchestrator/src/app.ts delete mode 100644 requester/orchestrator/src/test.ts delete mode 100644 requester/orchestrator/tsconfig.json diff --git a/docker-compose.yml b/docker-compose.yml index 322fe19..4063202 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.1.16 + image: randao/orchestrator:v0.1.50 depends_on: postgres: condition: service_healthy @@ -42,7 +42,7 @@ services: - ./wallet.json:/app/wallet.json # Mount wallet.json into the container # requester: - # image: randao/requester:v0.1.5 + # image: randao/requester:v0.1.6 # environment: # PATH_TO_WALLET: /app/wallet.json # Path inside the container # REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/orchestrator/package.json b/orchestrator/package.json index 01f8b49..67bb916 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^2.3.2", + "ao-process-clients": "^3", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", @@ -24,4 +24,4 @@ "keywords": [], "author": "", "license": "ISC" -} \ No newline at end of file +} diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index b80e888..31fd2ea 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -36,7 +36,7 @@ const MAX_OUTSTANDING_REQUESTS = 10; const MAX_OUTSTANDING_FULFILLMENTS = 10; const MAX_RETRIES = 10; const RETRY_DELAY_MS = 10000; -const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.0'; +const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.1'; const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks @@ -45,7 +45,7 @@ const ongoingFulfillments = new Set(); // Track request IDs for ongoing const PROVIDER_ID = process.env.PROVIDER_ID || "0"; let ongoingRequest = false; let spotInterruptions = 0; - +let totalProvided = 0; // Retry logic for connecting to PostgreSQL async function connectWithRetry(): Promise { const client = new Client(dbConfig); @@ -64,6 +64,13 @@ async function connectWithRetry(): Promise { // Setup the `verifiable_delay_functions` table if not exists async function setupDatabase(client: Client): Promise { + + // // Drop the table if it exists + // await client.query(` + // DROP TABLE IF EXISTS verifiable_delay_functions; + // `); + // console.log("'verifiable_delay_functions' table dropped."); + await client.query(` CREATE TABLE IF NOT EXISTS verifiable_delay_functions ( id TEXT PRIMARY KEY, -- Define as TEXT to match UUID format @@ -218,9 +225,10 @@ async function monitorDockerContainers(): Promise { async function checkAndFetchIfNeeded(client: Client): Promise { try { if (ongoingRequest) return; + const res = await client.query('SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL'); + const currentCount = parseInt(res.rows[0].count, 10); // Parse the count as an integer + console.log("Total usable db entries: " + currentCount); - const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); - const currentCount = parseInt(res.rows[0].count, 10); const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(currentCount); console.log("Updates onchain: " + updateAvailableValuesResult) console.log(await randclient.getProviderAvailableValues(PROVIDER_ID)) @@ -250,25 +258,66 @@ async function checkAndFetchIfNeeded(client: Client): Promise { ongoingRequest = false; } } -// Function to post VDF challenge and proof -async function fulfillRandomRequest(client: Client, dbId: string, requestId: string, modulus: string, input: string): Promise { + +// Function to clear all output requests +async function clearAllOutputRequests(client: Client): Promise { + try { + // Fetch the current open output requests + const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + console.log("Open requests fetched:", openRequests); + + if (openRequests && openRequests.activeOutputRequests) { + openRequests.activeOutputRequests.request_ids.forEach((requestId) => { + console.log(`Sending "No data" for output request ID: ${requestId}`); + + // Send "No data" as output for each request (do not await, fire and forget) + randclient.postVDFOutputAndProof(requestId, "No data", "No data").then(() => { + console.log(`"No data" sent for request ID: ${requestId}`); + }).catch((error) => { + console.error(`Error sending "No data" for request ID: ${requestId}:`, error); + }); + }); + } else { + console.log('No Output requests to clear'); + } + } catch (error) { + console.error('An error occurred while clearing output requests:', error); + } +} + + +// Function to post VDF challenge +async function fulfillRandomChallenge(client: Client, dbId: string, requestId: string, modulus: string, input: string): Promise { if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) return; - ongoingFulfillments.add(dbId); + try { + // Assume dbId, requestId, modulus, and input are passed in as parameters + + console.log(`Received entry details - ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); + + console.log(`Posting VDF challenge for entry ID: ${dbId}, request ID: ${requestId}`); await randclient.postVDFChallenge(requestId, modulus, input); console.log(`Challenge posted for request ID: ${requestId}. Waiting to post proof...`); - - await new Promise(resolve => setTimeout(resolve, 5000)); - - // Fetch the output and proof from the database - const res = await client.query('SELECT output, proof FROM verifiable_delay_functions WHERE id = $1', [dbId]); + } catch (error) { + console.error(`Error posting VDF challenge for request ID: ${requestId}:`, error); + } +} + + +// Function to post VDF output and proof +async function fulfillRandomOutput(client: Client, requestId: string): Promise { + try { + // Fetch the output and proof from the database using the requestId + const res = await client.query('SELECT id, output, proof FROM verifiable_delay_functions WHERE request_id = $1', [requestId]); if (!res.rowCount) { - console.error(`No entry found for ID: ${dbId}`); + console.error(`No entry found for request ID: ${requestId}`); return; } - const { output, proof } = res.rows[0]; + const { id: dbId, output, proof } = res.rows[0]; + // console.log(`Fetched entry from database for output - ID: ${dbId}, Output: ${output}, Proof: ${proof}`); + console.log(`Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}`); // Stringify the proof if it is an array const proofString = Array.isArray(proof) ? JSON.stringify(proof) : proof; @@ -276,51 +325,87 @@ async function fulfillRandomRequest(client: Client, dbId: string, requestId: str await randclient.postVDFOutputAndProof(requestId, output, proofString); console.log(`Proof posted for request ID: ${requestId}`); - // Delete the entry from the database after successfully posting the proof - await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [dbId]); - console.log(`Entry with ID: ${dbId} deleted from database.`); - } catch (error) { - console.error(`Error fulfilling random request for entry ID: ${dbId}, request ID: ${requestId}:`, error); - } finally { ongoingFulfillments.delete(dbId); + } catch (error) { + console.error(`Error fulfilling random output for request ID: ${requestId}:`, error); } } // Modified polling function to fulfill open requests if they exist async function polling(client: Client): Promise { + console.log("Starting Polling...") await checkAndFetchIfNeeded(client); try { console.log(PROVIDER_ID); + // Part 1: Fetch open requests const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - console.log("here"); - console.log(openRequests); - - if (openRequests && openRequests.activeRequests && openRequests.activeRequests.request_ids) { - console.log(openRequests.providerId); - console.log(openRequests.activeRequests); - console.log(openRequests.activeRequests.request_ids); - console.log(openRequests.activeRequests.request_ids.length); + console.log("Open requests fetched:", openRequests); + if (false) { + clearAllOutputRequests(client) + } + else { - // Check if there are enough entries in the database to fulfill the requests - const res = await client.query('SELECT * FROM verifiable_delay_functions ORDER BY id ASC LIMIT $1', [openRequests.activeRequests.request_ids.length]); - if (res.rowCount !== null && res.rowCount > 0) { - const rowsToProcess = Math.min(res.rowCount, openRequests.activeRequests.request_ids.length); - for (let i = 0; i < rowsToProcess; i++) { + // Part 2: Fulfill Challenge requests + if (openRequests && openRequests.activeChallengeRequests) { + for (const requestId of openRequests.activeChallengeRequests.request_ids) { if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) break; + try { + // Fetch modulus and input from the database + const res = await client.query('SELECT * FROM verifiable_delay_functions WHERE request_id IS NULL ORDER BY id ASC LIMIT 1'); + if (!res.rowCount) { + console.error(`No available entry found in the database.`); + return; + } + const { id: dbId, modulus, input } = res.rows[0]; + + // console.log(`Fetched entry from database - ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); + console.log(`Fetched entry from database - ID: ${dbId}, RequestID: ${requestId}`); + // Update the database to save the requestId with the entry + await client.query('UPDATE verifiable_delay_functions SET request_id = $1 WHERE id = $2', [requestId, dbId]); + console.log(`Updated database entry with request ID: ${requestId} for entry ID: ${dbId}`); + ongoingFulfillments.add(dbId); + console.log(`Processing challenge request ID: ${requestId}`); + fulfillRandomChallenge(client, dbId, requestId, modulus, input); + } catch (error) { + console.log(error) + } + } + } else { + console.log('No Challenge requests'); + } - const { id, modulus, input } = res.rows[i]; - const requestId = openRequests.activeRequests.request_ids[i]; - fulfillRandomRequest(client, id, requestId, modulus, input); + // Part 3: Fulfill Output requests + if (openRequests && openRequests.activeOutputRequests) { + for (const requestId of openRequests.activeOutputRequests.request_ids) { + console.log(`Processing output request ID: ${requestId}`); + fulfillRandomOutput(client, requestId); } } else { - console.log('Not enough entries in the database to fulfill all requests.'); + console.log('No Output requests'); + } + + // Part 4: Check for completed fulfillments that are no longer in the challenge or output list + for (const ongoingId of ongoingFulfillments) { + const challengeInProgress = openRequests.activeChallengeRequests?.request_ids.includes(ongoingId); + const outputInProgress = openRequests.activeOutputRequests?.request_ids.includes(ongoingId); + + console.log(`Checking ID: ${ongoingId}`); + console.log(` - In active challenges: ${challengeInProgress}`); + console.log(` - In active outputs: ${outputInProgress}`); + + if (!challengeInProgress && !outputInProgress) { + console.log(`Entry with ID: ${ongoingId} is no longer requested.`); + // await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [ongoingId]); + // console.log(`Entry with ID: ${ongoingId} deleted from database.`); + // ongoingFulfillments.delete(ongoingId); + // totalProvided++; + // console.log("total provided = " + totalProvided); + } } - } else { - console.log('No requests'); } } catch (error) { console.error('An error occurred while fetching open random requests:', error); @@ -328,6 +413,7 @@ async function polling(client: Client): Promise { } + // Main function async function run(): Promise { const client = await connectWithRetry(); diff --git a/orchestrator/src/test.ts b/orchestrator/src/test.ts deleted file mode 100644 index 2b56b81..0000000 --- a/orchestrator/src/test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { IRandomClient, RandomClient } from "ao-process-clients"; - -const PROVIDER_ID = process.env.PROVIDER_ID || "0"; - -async function main() { - try { - const randclient: IRandomClient = RandomClient.autoConfiguration(); - - console.log("Testing `createRequest`..."); - const createRequestResult = await randclient.createRequest(["provider1", "provider2"]); - console.log("createRequest result:", createRequestResult); - - console.log("Testing `getOpenRandomRequests`..."); - const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - console.log("getOpenRandomRequests result:", openRequests); - - console.log("Testing `getProviderAvailableValues`..."); - const availableValues = await randclient.getProviderAvailableValues(PROVIDER_ID); - console.log("getProviderAvailableValues result:", availableValues); - - console.log("Testing `getRandomRequests`..."); - const randomRequests = await randclient.getRandomRequests(["request1", "request2"]); - console.log("getRandomRequests result:", randomRequests); - - console.log("Testing `postVDFChallenge`..."); - const postVDFChallengeResult = await randclient.postVDFChallenge( - "request1", - "modulus_value", - "input_value" - ); - console.log("postVDFChallenge result:", postVDFChallengeResult); - - console.log("Testing `postVDFOutputAndProof`..."); - const postVDFOutputAndProofResult = await randclient.postVDFOutputAndProof( - "request1", - "output_value", - "proof_value" - ); - console.log("postVDFOutputAndProof result:", postVDFOutputAndProofResult); - - console.log("Testing `updateProviderAvailableValues`..."); - const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(42); - console.log("updateProviderAvailableValues result:", updateAvailableValuesResult); - - } catch (error) { - console.error("An error occurred during testing:", error); - } -} - -// Call the main function -main(); diff --git a/requester/orchestrator/.dockerignore b/requester/orchestrator/.dockerignore deleted file mode 100644 index a0bedda..0000000 --- a/requester/orchestrator/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -.git -dist diff --git a/requester/orchestrator/Dockerfile b/requester/orchestrator/Dockerfile deleted file mode 100644 index e0b8740..0000000 --- a/requester/orchestrator/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Use the official Node.js image as the base image -FROM node:20 - -# Create and set the working directory -WORKDIR /usr/src/app - -# Copy package.json and package-lock.json -COPY package*.json ./ - -# Install dependencies -RUN npm install - -# Copy the entire project into the container -COPY . . - -# Compile TypeScript to JavaScript -RUN npx tsc - -# Expose the port if needed -EXPOSE 3000 - -# Run the compiled app -CMD ["node", "dist/app.js"] diff --git a/requester/orchestrator/README.md b/requester/orchestrator/README.md deleted file mode 100644 index 5470a55..0000000 --- a/requester/orchestrator/README.md +++ /dev/null @@ -1,6 +0,0 @@ -cd into repo - -docker login -docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.1.4 . -docker push satoshispalace/orchestrator:v0.1.4 -docker push satoshispalace/orchestrator:latest diff --git a/requester/orchestrator/docs/development.md b/requester/orchestrator/docs/development.md deleted file mode 100644 index e69de29..0000000 diff --git a/requester/orchestrator/package.json b/requester/orchestrator/package.json deleted file mode 100644 index 01f8b49..0000000 --- a/requester/orchestrator/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "devDependencies": { - "@types/dockerode": "^3.3.31", - "@types/node": "^22.9.1", - "@types/pg": "^8.11.10", - "serverless-offline": "^14.3.3", - "typescript": "^5.6.3" - }, - "dependencies": { - "ao-process-clients": "^2.3.2", - "aws-sdk": "^2.1692.0", - "axios": "^1.7.7", - "crypto": "^1.0.1", - "dockerode": "^4.0.2", - "pg": "^8.13.1" - }, - "name": "ao-vrf", - "description": "1. To build:\r ```\r docker build -t serverless-multi-cloud .\r ```", - "version": "1.0.0", - "main": "Organizer.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC" -} \ No newline at end of file diff --git a/requester/orchestrator/src/app.ts b/requester/orchestrator/src/app.ts deleted file mode 100644 index 33c4cab..0000000 --- a/requester/orchestrator/src/app.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { Client } from 'pg'; -import Docker from 'dockerode'; -import AWS from 'aws-sdk'; -import { getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" - -// const RANDOM_CONFIG: RandomClientConfig = { -// tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, -// processId: getRandomClientAutoConfiguration().processId, -// wallet: JSON.parse(process.env.WALLET_JSON!), -// environment: 'mainnet' -// } -const randclient: IRandomClient = RandomClient.autoConfiguration() -//const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG) - -const docker = new Docker(); - -// Database configuration -const dbConfig = { - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432', 10), - user: process.env.DB_USER || 'myuser', - password: process.env.DB_PASSWORD || 'mypassword', - database: process.env.DB_NAME || 'mydatabase', -}; - -// Constants for configuration -const POLLING_INTERVAL_MS = 100; -const MINIMUM_ENTRIES = 50; -const TARGET_ENTRIES = 75; -const DROP_CHANCE = 0.005; -//Expected increments per second=10×0.005=0.05 -//180 times per hour -//4,320 times per day -//1,576,800 times per year -const MAX_OUTSTANDING_REQUESTS = 10; -const MAX_RETRIES = 10; -const RETRY_DELAY_MS = 10000; -const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.0'; -const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; -const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); -const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks -const ongoingContainers = new Set(); // Track container IDs of running Docker containers -const PROVIDER_ID = process.env.PROVIDER_ID || "0"; - - - -let ongoingRequest = false; - -// Retry logic for connecting to PostgreSQL -async function connectWithRetry(): Promise { - const client = new Client(dbConfig); - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { - try { - await client.connect(); - console.log(`Connected to PostgreSQL database (Attempt ${attempt})`); - return client; - } catch (error) { - console.error(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); - } - } - throw new Error("Failed to connect to PostgreSQL after multiple attempts"); -} - -// Setup the `verifiable_delay_functions` table if not exists -async function setupDatabase(client: Client): Promise { - await client.query(` - CREATE TABLE IF NOT EXISTS verifiable_delay_functions ( - id TEXT PRIMARY KEY, -- Define as TEXT to match UUID format - request_id TEXT, - modulus TEXT NOT NULL, - input TEXT NOT NULL, - output TEXT NOT NULL, - proof JSON NOT NULL, - date TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - `); - console.log("Database setup complete. 'verifiable_delay_functions' table is ready."); -} - -// Added variable to track the number of Spot instance interruptions -let spotInterruptions = 0; - -// Modified function to trigger VDF job pod using ECS or Docker -async function triggerVDFJobPod(): Promise { - if (ENVIRONMENT === 'cloud') { - try { - console.log("Cloud environment detected. Launching ECS task."); - const result = await ecs.runTask({ - cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - taskDefinition: 'vdf-job', - capacityProviderStrategy: [ - { - capacityProvider: 'FARGATE_SPOT', - weight: 1 - } - ], - networkConfiguration: { - awsvpcConfiguration: { - subnets: [process.env.SUBNET_ID || 'subnet-12345678'], - securityGroups: [process.env.SECURITY_GROUP || 'sg-12345678'], - assignPublicIp: 'ENABLED' - } - }, - overrides: { - containerOverrides: [ - { - name: 'vdf_job_container', - environment: [ - { name: 'DATABASE_TYPE', value: 'postgresql' }, - { name: 'DATABASE_HOST', value: process.env.DB_HOST || 'cloud-postgres-host' }, - { name: 'DATABASE_PORT', value: process.env.DB_PORT || '5432' }, - { name: 'DATABASE_USER', value: process.env.DB_USER || 'myuser' }, - { name: 'DATABASE_PASSWORD', value: process.env.DB_PASSWORD || 'mypassword' }, - { name: 'DATABASE_NAME', value: process.env.DB_NAME || 'mydatabase' }, - ] - } - ] - }, - count: 1 - }).promise(); - - const taskArn = result.tasks?.[0]?.taskArn; - if (taskArn) { - ongoingTasks.add(taskArn); - console.log(`ECS task started successfully: ${taskArn}`); - return taskArn; - } - return null; - } catch (error: any) { - if (error?.code === 'SpotCapacityNotAvailableException') { - spotInterruptions++; - console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } else { - console.error("Error launching ECS task:", error); - } - return null; - } - } else { - const containerName = `vdf_job_${Date.now()}`; - console.log(`Starting Docker container with name: ${containerName}`); - const container = await docker.createContainer({ - Image: VDF_JOB_IMAGE, - Cmd: ['python', 'main.py'], - Env: [ - `DATABASE_TYPE=postgresql`, - `DATABASE_HOST=${dbConfig.host}`, - `DATABASE_PORT=${dbConfig.port.toString()}`, - `DATABASE_USER=${dbConfig.user}`, - `DATABASE_PASSWORD=${dbConfig.password}`, - `DATABASE_NAME=${dbConfig.database}`, - ], - HostConfig: { - NetworkMode: 'backend', - }, - name: containerName - }); - - await container.start(); - ongoingContainers.add(container.id); - console.log(`Docker container ${containerName} started successfully.`); - return container.id; - } -} - -// Modified function to wait for ECS tasks to complete and remove them from tracking -async function monitorECSTasks(): Promise { - if (ongoingTasks.size === 0) return; - - const describeTasksResult = await ecs.describeTasks({ - cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - tasks: Array.from(ongoingTasks) - }).promise(); - - describeTasksResult.tasks?.forEach(task => { - // Check capacity provider name to determine if it is running Fargate or Fargate Spot - const capacityProvider = task.capacityProviderName || 'Unknown'; - - console.log(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); - - if (task.lastStatus === 'STOPPED') { - console.log(`ECS task stopped: ${task.taskArn}`); - if (task.stoppedReason) { - console.log(`Task stopped reason: ${task.stoppedReason}`); - if (task.stoppedReason.includes('Host EC2 instance termination')) { - spotInterruptions++; - console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } - } else { - console.log('Task stopped due to normal completion.'); - } - ongoingTasks.delete(task.taskArn as string); - } - }); -} - - - - -// Function to wait for Docker containers to complete and remove them from tracking -async function monitorDockerContainers(): Promise { - if (ongoingContainers.size === 0) return; - - for (const containerId of ongoingContainers) { - try { - const container = docker.getContainer(containerId); - const containerInfo = await container.inspect(); - - if (containerInfo.State.Status === 'exited') { - console.log(`Docker container stopped: ${containerId}`); - await container.remove(); - ongoingContainers.delete(containerId); - } - } catch (error) { - console.error(`Error monitoring Docker container ${containerId}:`, error); - ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) - } - } -} - -// Function to check if there are fewer than MINIMUM_ENTRIES and fetch until TARGET_ENTRIES -async function checkAndFetchIfNeeded(client: Client): Promise { - try { - if (ongoingRequest) return; - - const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); - const currentCount = parseInt(res.rows[0].count, 10); - const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(currentCount); - console.log("Updates onchain: " + updateAvailableValuesResult) - console.log(await randclient.getProviderAvailableValues(PROVIDER_ID)) - if (currentCount < MINIMUM_ENTRIES) { - const entriesNeeded = TARGET_ENTRIES - currentCount; - console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries to reach ${TARGET_ENTRIES}...`); - ongoingRequest = true; - let tasksTriggered = 0; - - while (tasksTriggered < entriesNeeded) { - if (ENVIRONMENT === 'cloud' && ongoingTasks.size >= MAX_OUTSTANDING_REQUESTS) { - await monitorECSTasks(); - } else if (ENVIRONMENT !== 'cloud' && ongoingContainers.size >= MAX_OUTSTANDING_REQUESTS) { - await monitorDockerContainers(); - } else { - const taskArn = await triggerVDFJobPod(); - if (taskArn) { - tasksTriggered++; - console.log(`Task triggered: ${taskArn}. Total ongoing tasks: ${ongoingTasks.size}. Total ongoing containers ${+ ongoingContainers.size}`); - } - } - } - ongoingRequest = false; - } - } catch (error) { - console.error('Error during check and fetch:', error); - ongoingRequest = false; - } -} - -// Polling function to manage entries and delete old ones occasionally -async function polling(client: Client): Promise { - await checkAndFetchIfNeeded(client); - - console.log(PROVIDER_ID) - var openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID) - console.log(openRequests) - console.log(openRequests.providerId) - console.log(openRequests.activeRequests) - console.log(openRequests.activeRequests.request_ids) - console.log(openRequests.activeRequests.request_ids.length) - - if (Math.random() < DROP_CHANCE) { - try { - console.log("Randomly chosen to log and delete the oldest entry..."); - const res = await client.query('SELECT * FROM verifiable_delay_functions ORDER BY id ASC LIMIT 1'); - const entry = res.rows[0]; - - if (entry) { - console.log("Logging and deleting oldest entry:", JSON.stringify(entry, null, 2)); - await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [entry.id]); - console.log("Oldest entry deleted from database."); - } else { - console.log("No entries available to delete."); - } - } catch (error) { - console.error('Error logging and deleting oldest entry:', error); - } - } else { - } -} - -// Main function -async function run(): Promise { - const client = await connectWithRetry(); - await setupDatabase(client); - - setInterval(async () => { - const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); - console.log(`Periodic log - Current database size: ${res.rows[0].count}`); - }, 10000); - - setInterval(async () => { - await polling(client); - }, POLLING_INTERVAL_MS); - - - - process.on("SIGTERM", async () => { - console.log("SIGTERM received. Closing database connection."); - await client.end(); - process.exit(0); - }); -} - -run().catch((err) => console.error(`Error in main function: ${err}`)); diff --git a/requester/orchestrator/src/test.ts b/requester/orchestrator/src/test.ts deleted file mode 100644 index 2b56b81..0000000 --- a/requester/orchestrator/src/test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { IRandomClient, RandomClient } from "ao-process-clients"; - -const PROVIDER_ID = process.env.PROVIDER_ID || "0"; - -async function main() { - try { - const randclient: IRandomClient = RandomClient.autoConfiguration(); - - console.log("Testing `createRequest`..."); - const createRequestResult = await randclient.createRequest(["provider1", "provider2"]); - console.log("createRequest result:", createRequestResult); - - console.log("Testing `getOpenRandomRequests`..."); - const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - console.log("getOpenRandomRequests result:", openRequests); - - console.log("Testing `getProviderAvailableValues`..."); - const availableValues = await randclient.getProviderAvailableValues(PROVIDER_ID); - console.log("getProviderAvailableValues result:", availableValues); - - console.log("Testing `getRandomRequests`..."); - const randomRequests = await randclient.getRandomRequests(["request1", "request2"]); - console.log("getRandomRequests result:", randomRequests); - - console.log("Testing `postVDFChallenge`..."); - const postVDFChallengeResult = await randclient.postVDFChallenge( - "request1", - "modulus_value", - "input_value" - ); - console.log("postVDFChallenge result:", postVDFChallengeResult); - - console.log("Testing `postVDFOutputAndProof`..."); - const postVDFOutputAndProofResult = await randclient.postVDFOutputAndProof( - "request1", - "output_value", - "proof_value" - ); - console.log("postVDFOutputAndProof result:", postVDFOutputAndProofResult); - - console.log("Testing `updateProviderAvailableValues`..."); - const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(42); - console.log("updateProviderAvailableValues result:", updateAvailableValuesResult); - - } catch (error) { - console.error("An error occurred during testing:", error); - } -} - -// Call the main function -main(); diff --git a/requester/orchestrator/tsconfig.json b/requester/orchestrator/tsconfig.json deleted file mode 100644 index 493e143..0000000 --- a/requester/orchestrator/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "module": "commonjs", - "target": "es6", - "strict": true, - "esModuleInterop": true - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file diff --git a/requester/package.json b/requester/package.json index 01f8b49..68a050e 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^2.3.2", + "ao-process-clients": "^3", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/requester/src/app.ts b/requester/src/app.ts index 1e98b18..3b71834 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -30,56 +30,57 @@ async function main() { // Roll for random chance to make a request if (Math.random() < CHANCE_TO_CALL_RANDOM) { console.log("Initiating random request..."); - await randclient.createRequest([PROVIDER_ID]); + const callbackId = `callback-${Date.now()}`; + await randclient.createRequest([PROVIDER_ID], 1, callbackId); totalRandomCalled++; console.log("Random request initiated. Awaiting request ID in open requests..."); } - // Check open requests - const openRequestsResponse = await randclient.getOpenRandomRequests(PROVIDER_ID); - const openRequestIds = openRequestsResponse.activeRequests.request_ids || []; - console.log("Open requests:", openRequestIds); + // // Check open requests + // const openRequestsResponse = await randclient.getOpenRandomRequests(PROVIDER_ID); + // const openRequestIds = openRequestsResponse.activeRequests.request_ids || []; + // console.log("Open requests:", openRequestIds); - // Track outstanding requests - for (const requestId of openRequestIds) { - if (!outstandingRequests.has(requestId)) { - console.log(`Tracking new request: ${requestId}`); - outstandingRequests.add(requestId); - } - } + // // Track outstanding requests + // for (const requestId of openRequestIds) { + // if (!outstandingRequests.has(requestId)) { + // console.log(`Tracking new request: ${requestId}`); + // outstandingRequests.add(requestId); + // } + // } - // Check the status of outstanding requests - if (outstandingRequests.size > 0) { - const randomRequestsResponse = await randclient.getRandomRequests(Array.from(outstandingRequests)); - const requests = randomRequestsResponse.randomRequestResponses || []; // Adjust based on actual response structure - console.log(randomRequestsResponse) - console.log(requests) + // // Check the status of outstanding requests + // if (outstandingRequests.size > 0) { + // const randomRequestsResponse = await randclient.getRandomRequests(Array.from(outstandingRequests)); + // const requests = randomRequestsResponse.randomRequestResponses || []; // Adjust based on actual response structure + // console.log(randomRequestsResponse) + // console.log(requests) - // for (const request of requests) { - // const requestId = request.requestId; // Adjust if property has a different name - // if (request?.status === "fulfilled") { - // const fulfilledTime = Date.now(); - // const timeToFulfill = fulfilledTime - request.createdTime; // Adjust if createdTime exists - // totalTimeToFulfill += timeToFulfill; - // fulfilledRequests++; - // console.log(`Request ${requestId} fulfilled. Time to fulfill: ${timeToFulfill}ms`); - // outstandingRequests.delete(requestId); // Stop tracking fulfilled requests - // } else { - // console.log(`Request ${requestId} is still being processed.`); - // } - // } - } + // // for (const request of requests) { + // // const requestId = request.requestId; // Adjust if property has a different name + // // if (request?.status === "fulfilled") { + // // const fulfilledTime = Date.now(); + // // const timeToFulfill = fulfilledTime - request.createdTime; // Adjust if createdTime exists + // // totalTimeToFulfill += timeToFulfill; + // // fulfilledRequests++; + // // console.log(`Request ${requestId} fulfilled. Time to fulfill: ${timeToFulfill}ms`); + // // outstandingRequests.delete(requestId); // Stop tracking fulfilled requests + // // } else { + // // console.log(`Request ${requestId} is still being processed.`); + // // } + // // } + // } - // Calculate and log stats - if (fulfilledRequests > 0) { - const avgTimeToFulfill = totalTimeToFulfill / fulfilledRequests; + // // Calculate and log stats + // if (fulfilledRequests > 0) { + // const avgTimeToFulfill = totalTimeToFulfill / fulfilledRequests; - console.log(` - Total Random Called: ${totalRandomCalled} - Outstanding Requests: ${outstandingRequests.size} - Average Time to Fulfill: ${avgTimeToFulfill}ms - `); - } + // console.log(` + // Total Random Called: ${totalRandomCalled} + // Outstanding Requests: ${outstandingRequests.size} + // Average Time to Fulfill: ${avgTimeToFulfill}ms + // `); + // } // Wait before next cycle await delay(RETRY_DELAY_MS); diff --git a/verifiable-delay-function/src/protocol_constants.py b/verifiable-delay-function/src/protocol_constants.py index 4cc3e00..ba1788b 100644 --- a/verifiable-delay-function/src/protocol_constants.py +++ b/verifiable-delay-function/src/protocol_constants.py @@ -1,5 +1,5 @@ # protocol_constants.py BIT_SIZE = 2048 # RSA modulus bit size -TOTAL_SQUARINGS = 10000000 # T - Total squarings for delay +TOTAL_SQUARINGS = 3000000 # T - Total squarings for delay NUM_SEGMENTS = 10 # Number of segments for parallel verification From 0f684dd3618fbadd05dfbccf46f9a5c37dbeff4a Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 19 Dec 2024 14:09:34 -0500 Subject: [PATCH 08/80] added new features --- docker-compose.yml | 22 +++++- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 77 +++++++++++++------ requester/package.json | 2 +- .../src/protocol_constants.py | 4 +- 5 files changed, 76 insertions(+), 31 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4063202..8a2df54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.1.50 + image: randao/orchestrator:v0.1.63 depends_on: postgres: condition: service_healthy @@ -42,7 +42,7 @@ services: - ./wallet.json:/app/wallet.json # Mount wallet.json into the container # requester: - # image: randao/requester:v0.1.6 + # image: randao/requester:v0.1.11 # environment: # PATH_TO_WALLET: /app/wallet.json # Path inside the container # REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} @@ -53,6 +53,22 @@ services: # volumes: # - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + dbeaver: + image: dbeaver/cloudbeaver:23.2.0 + environment: + CB_SERVER_SERVER_PORT: 8080 + CB_SERVER_ADMIN_NAME: "admin" + CB_SERVER_ADMIN_PASSWORD: "admin123" + depends_on: + postgres: + condition: service_healthy + networks: + - backend + ports: + - "8081:8978" # Expose the DBeaver web UI + volumes: + - dbeaver-data:/opt/cloudbeaver/workspace + networks: backend: name: backend # This will set the network name explicitly @@ -61,3 +77,5 @@ networks: volumes: pgdata: driver: local + dbeaver-data: + driver: local diff --git a/orchestrator/package.json b/orchestrator/package.json index 67bb916..d1ef1d0 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^3", + "ao-process-clients":"3.4.2", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 31fd2ea..623bbf9 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -25,18 +25,17 @@ const dbConfig = { // Constants for configuration const POLLING_INTERVAL_MS = 5000; -const MINIMUM_ENTRIES = 50; -const TARGET_ENTRIES = 75; -const DROP_CHANCE = 0.005; +const MINIMUM_ENTRIES = 250; +const TARGET_ENTRIES = 500; //Expected increments per second=10×0.005=0.05 //180 times per hour //4,320 times per day //1,576,800 times per year -const MAX_OUTSTANDING_REQUESTS = 10; -const MAX_OUTSTANDING_FULFILLMENTS = 10; +const MAX_OUTSTANDING_REQUESTS = 50; +const MAX_OUTSTANDING_FULFILLMENTS = 50; const MAX_RETRIES = 10; const RETRY_DELAY_MS = 10000; -const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.1'; +const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.2'; const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks @@ -209,18 +208,39 @@ async function monitorDockerContainers(): Promise { const container = docker.getContainer(containerId); const containerInfo = await container.inspect(); + // Check if the container is already stopped (exited) if (containerInfo.State.Status === 'exited') { console.log(`Docker container stopped: ${containerId}`); - await container.remove(); - ongoingContainers.delete(containerId); + + // Attempt to remove the container, handling possible errors gracefully + try { + await container.remove({ force: true }); // Force removal to avoid "in progress" errors + console.log(`Docker container removed: ${containerId}`); + ongoingContainers.delete(containerId); + } catch (removeError) { + if (isDockerError(removeError) && removeError.statusCode === 409) { + // Error 409 means removal is in progress, so skip this container for now + console.log(`Removal of container ${containerId} is already in progress. Skipping.`); + } else { + // Handle other errors that might occur during container removal + console.error(`Error removing Docker container ${containerId}:`, removeError); + } + } } } catch (error) { - console.error(`Error monitoring Docker container ${containerId}:`, error); + console.error(`Error inspecting Docker container ${containerId}:`, error); ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) } } } +// Helper function to type guard Docker errors +function isDockerError(error: unknown): error is { statusCode: number } { + return typeof error === 'object' && error !== null && 'statusCode' in error && typeof (error as any).statusCode === 'number'; +} + + + // Function to check if there are fewer than MINIMUM_ENTRIES and fetch until TARGET_ENTRIES async function checkAndFetchIfNeeded(client: Client): Promise { try { @@ -321,7 +341,7 @@ async function fulfillRandomOutput(client: Client, requestId: string): Promise { - console.log("Starting Polling...") + console.log("Starting Polling..."); await checkAndFetchIfNeeded(client); try { @@ -342,16 +362,15 @@ async function polling(client: Client): Promise { // Part 1: Fetch open requests const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); console.log("Open requests fetched:", openRequests); - if (false) { - clearAllOutputRequests(client) - } - else { - + if (false) { + clearAllOutputRequests(client); + } else { // Part 2: Fulfill Challenge requests if (openRequests && openRequests.activeChallengeRequests) { for (const requestId of openRequests.activeChallengeRequests.request_ids) { + console.log("Max outstanding is "+ MAX_OUTSTANDING_FULFILLMENTS) if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) break; try { // Fetch modulus and input from the database @@ -362,16 +381,15 @@ async function polling(client: Client): Promise { } const { id: dbId, modulus, input } = res.rows[0]; - // console.log(`Fetched entry from database - ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); console.log(`Fetched entry from database - ID: ${dbId}, RequestID: ${requestId}`); // Update the database to save the requestId with the entry await client.query('UPDATE verifiable_delay_functions SET request_id = $1 WHERE id = $2', [requestId, dbId]); console.log(`Updated database entry with request ID: ${requestId} for entry ID: ${dbId}`); ongoingFulfillments.add(dbId); console.log(`Processing challenge request ID: ${requestId}`); - fulfillRandomChallenge(client, dbId, requestId, modulus, input); + await fulfillRandomChallenge(client, dbId, requestId, modulus, input); } catch (error) { - console.log(error) + console.log(error); } } } else { @@ -388,7 +406,9 @@ async function polling(client: Client): Promise { console.log('No Output requests'); } + // Part 4: Check for completed fulfillments that are no longer in the challenge or output list + const noLongerUsedIds: string[] = []; for (const ongoingId of ongoingFulfillments) { const challengeInProgress = openRequests.activeChallengeRequests?.request_ids.includes(ongoingId); const outputInProgress = openRequests.activeOutputRequests?.request_ids.includes(ongoingId); @@ -398,12 +418,18 @@ async function polling(client: Client): Promise { console.log(` - In active outputs: ${outputInProgress}`); if (!challengeInProgress && !outputInProgress) { - console.log(`Entry with ID: ${ongoingId} is no longer requested.`); - // await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [ongoingId]); - // console.log(`Entry with ID: ${ongoingId} deleted from database.`); - // ongoingFulfillments.delete(ongoingId); - // totalProvided++; - // console.log("total provided = " + totalProvided); + noLongerUsedIds.push(ongoingId); + } + } + + // Log all IDs that are no longer in use in a single line + if (noLongerUsedIds.length > 0) { + console.log(`No longer in use: ${noLongerUsedIds.join(', ')}`); + // Optionally remove them from the ongoing fulfillments list if necessary + for (const id of noLongerUsedIds) { + console.log("Removing id from list:" + id) + //await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [id]); + ongoingFulfillments.delete(id); } } } @@ -414,6 +440,7 @@ async function polling(client: Client): Promise { + // Main function async function run(): Promise { const client = await connectWithRetry(); diff --git a/requester/package.json b/requester/package.json index 68a050e..e502c8a 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^3", + "ao-process-clients": "3.4.2", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/verifiable-delay-function/src/protocol_constants.py b/verifiable-delay-function/src/protocol_constants.py index ba1788b..1157d0c 100644 --- a/verifiable-delay-function/src/protocol_constants.py +++ b/verifiable-delay-function/src/protocol_constants.py @@ -1,5 +1,5 @@ # protocol_constants.py -BIT_SIZE = 2048 # RSA modulus bit size -TOTAL_SQUARINGS = 3000000 # T - Total squarings for delay +BIT_SIZE = 1024 # RSA modulus bit size +TOTAL_SQUARINGS = 10000 # T - Total squarings for delay NUM_SEGMENTS = 10 # Number of segments for parallel verification From 75bbbdd7575b88a34a3ae204150457f1e5dc0325 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 20 Dec 2024 23:54:50 -0500 Subject: [PATCH 09/80] V1 working --- docker-compose.yml | 24 +- orchestrator/src/app.ts | 962 +++++++++--------- requester/src/app.ts | 2 +- .../src/protocol_constants.py | 4 +- 4 files changed, 512 insertions(+), 480 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8a2df54..40d406d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.1.63 + image: randao/orchestrator:v0.1.78 depends_on: postgres: condition: service_healthy @@ -41,17 +41,17 @@ services: - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket - ./wallet.json:/app/wallet.json # Mount wallet.json into the container - # requester: - # image: randao/requester:v0.1.11 - # environment: - # PATH_TO_WALLET: /app/wallet.json # Path inside the container - # REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - # PROVIDER_ID: ${PROVIDER_ID} - # DOCKER_NETWORK: backend # Passing the network name - # networks: - # - backend - # volumes: - # - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + requester: + image: randao/requester:v0.1.13 + environment: + PATH_TO_WALLET: /app/wallet.json # Path inside the container + REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} + PROVIDER_ID: ${PROVIDER_ID} + DOCKER_NETWORK: backend # Passing the network name + networks: + - backend + volumes: + - ./wallet.json:/app/wallet.json # Mount wallet.json into the container dbeaver: image: dbeaver/cloudbeaver:23.2.0 diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 623bbf9..ea542dd 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,465 +1,497 @@ -import { Client } from 'pg'; -import Docker from 'dockerode'; -import AWS from 'aws-sdk'; -import { getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" - -// const RANDOM_CONFIG: RandomClientConfig = { -// tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, -// processId: getRandomClientAutoConfiguration().processId, -// wallet: JSON.parse(process.env.WALLET_JSON!), -// environment: 'mainnet' -// } -const randclient: IRandomClient = RandomClient.autoConfiguration() -//const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG) - -const docker = new Docker(); - -// Database configuration -const dbConfig = { - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432', 10), - user: process.env.DB_USER || 'myuser', - password: process.env.DB_PASSWORD || 'mypassword', - database: process.env.DB_NAME || 'mydatabase', -}; - -// Constants for configuration -const POLLING_INTERVAL_MS = 5000; -const MINIMUM_ENTRIES = 250; -const TARGET_ENTRIES = 500; -//Expected increments per second=10×0.005=0.05 -//180 times per hour -//4,320 times per day -//1,576,800 times per year -const MAX_OUTSTANDING_REQUESTS = 50; -const MAX_OUTSTANDING_FULFILLMENTS = 50; -const MAX_RETRIES = 10; -const RETRY_DELAY_MS = 10000; -const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.2'; -const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; -const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); -const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks -const ongoingContainers = new Set(); // Track container IDs of running Docker containers -const ongoingFulfillments = new Set(); // Track request IDs for ongoing fulfillments -const PROVIDER_ID = process.env.PROVIDER_ID || "0"; -let ongoingRequest = false; -let spotInterruptions = 0; -let totalProvided = 0; -// Retry logic for connecting to PostgreSQL -async function connectWithRetry(): Promise { - const client = new Client(dbConfig); - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { - try { - await client.connect(); - console.log(`Connected to PostgreSQL database (Attempt ${attempt})`); - return client; - } catch (error) { - console.error(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); - } - } - throw new Error("Failed to connect to PostgreSQL after multiple attempts"); -} - -// Setup the `verifiable_delay_functions` table if not exists -async function setupDatabase(client: Client): Promise { - - // // Drop the table if it exists - // await client.query(` - // DROP TABLE IF EXISTS verifiable_delay_functions; - // `); - // console.log("'verifiable_delay_functions' table dropped."); - - await client.query(` - CREATE TABLE IF NOT EXISTS verifiable_delay_functions ( - id TEXT PRIMARY KEY, -- Define as TEXT to match UUID format - request_id TEXT, - modulus TEXT NOT NULL, - input TEXT NOT NULL, - output TEXT NOT NULL, - proof JSON NOT NULL, - date TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - `); - console.log("Database setup complete. 'verifiable_delay_functions' table is ready."); -} - - -// Modified function to trigger VDF job pod using ECS or Docker -async function triggerVDFJobPod(): Promise { - if (ENVIRONMENT === 'cloud') { - try { - console.log("Cloud environment detected. Launching ECS task."); - const result = await ecs.runTask({ - cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - taskDefinition: 'vdf-job', - capacityProviderStrategy: [ - { - capacityProvider: 'FARGATE_SPOT', - weight: 1 - } - ], - networkConfiguration: { - awsvpcConfiguration: { - subnets: [process.env.SUBNET_ID || 'subnet-12345678'], - securityGroups: [process.env.SECURITY_GROUP || 'sg-12345678'], - assignPublicIp: 'ENABLED' - } - }, - overrides: { - containerOverrides: [ - { - name: 'vdf_job_container', - environment: [ - { name: 'DATABASE_TYPE', value: 'postgresql' }, - { name: 'DATABASE_HOST', value: process.env.DB_HOST || 'cloud-postgres-host' }, - { name: 'DATABASE_PORT', value: process.env.DB_PORT || '5432' }, - { name: 'DATABASE_USER', value: process.env.DB_USER || 'myuser' }, - { name: 'DATABASE_PASSWORD', value: process.env.DB_PASSWORD || 'mypassword' }, - { name: 'DATABASE_NAME', value: process.env.DB_NAME || 'mydatabase' }, - ] - } - ] - }, - count: 1 - }).promise(); - - const taskArn = result.tasks?.[0]?.taskArn; - if (taskArn) { - ongoingTasks.add(taskArn); - console.log(`ECS task started successfully: ${taskArn}`); - return taskArn; - } - return null; - } catch (error: any) { - if (error?.code === 'SpotCapacityNotAvailableException') { - spotInterruptions++; - console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } else { - console.error("Error launching ECS task:", error); - } - return null; - } - } else { - const containerName = `vdf_job_${Date.now()}`; - console.log(`Starting Docker container with name: ${containerName}`); - const container = await docker.createContainer({ - Image: VDF_JOB_IMAGE, - Cmd: ['python', 'main.py'], - Env: [ - `DATABASE_TYPE=postgresql`, - `DATABASE_HOST=${dbConfig.host}`, - `DATABASE_PORT=${dbConfig.port.toString()}`, - `DATABASE_USER=${dbConfig.user}`, - `DATABASE_PASSWORD=${dbConfig.password}`, - `DATABASE_NAME=${dbConfig.database}`, - ], - HostConfig: { - NetworkMode: 'backend', - }, - name: containerName - }); - - await container.start(); - ongoingContainers.add(container.id); - console.log(`Docker container ${containerName} started successfully.`); - return container.id; - } -} - -// Modified function to wait for ECS tasks to complete and remove them from tracking -async function monitorECSTasks(): Promise { - if (ongoingTasks.size === 0) return; - - const describeTasksResult = await ecs.describeTasks({ - cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - tasks: Array.from(ongoingTasks) - }).promise(); - - describeTasksResult.tasks?.forEach(task => { - // Check capacity provider name to determine if it is running Fargate or Fargate Spot - const capacityProvider = task.capacityProviderName || 'Unknown'; - - console.log(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); - - if (task.lastStatus === 'STOPPED') { - console.log(`ECS task stopped: ${task.taskArn}`); - if (task.stoppedReason) { - console.log(`Task stopped reason: ${task.stoppedReason}`); - if (task.stoppedReason.includes('Host EC2 instance termination')) { - spotInterruptions++; - console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } - } else { - console.log('Task stopped due to normal completion.'); - } - ongoingTasks.delete(task.taskArn as string); - } - }); -} - - -// Function to wait for Docker containers to complete and remove them from tracking -async function monitorDockerContainers(): Promise { - if (ongoingContainers.size === 0) return; - - for (const containerId of ongoingContainers) { - try { - const container = docker.getContainer(containerId); - const containerInfo = await container.inspect(); - - // Check if the container is already stopped (exited) - if (containerInfo.State.Status === 'exited') { - console.log(`Docker container stopped: ${containerId}`); - - // Attempt to remove the container, handling possible errors gracefully - try { - await container.remove({ force: true }); // Force removal to avoid "in progress" errors - console.log(`Docker container removed: ${containerId}`); - ongoingContainers.delete(containerId); - } catch (removeError) { - if (isDockerError(removeError) && removeError.statusCode === 409) { - // Error 409 means removal is in progress, so skip this container for now - console.log(`Removal of container ${containerId} is already in progress. Skipping.`); - } else { - // Handle other errors that might occur during container removal - console.error(`Error removing Docker container ${containerId}:`, removeError); - } - } - } - } catch (error) { - console.error(`Error inspecting Docker container ${containerId}:`, error); - ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) - } - } -} - -// Helper function to type guard Docker errors -function isDockerError(error: unknown): error is { statusCode: number } { - return typeof error === 'object' && error !== null && 'statusCode' in error && typeof (error as any).statusCode === 'number'; -} - - - -// Function to check if there are fewer than MINIMUM_ENTRIES and fetch until TARGET_ENTRIES -async function checkAndFetchIfNeeded(client: Client): Promise { - try { - if (ongoingRequest) return; - const res = await client.query('SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL'); - const currentCount = parseInt(res.rows[0].count, 10); // Parse the count as an integer - console.log("Total usable db entries: " + currentCount); - - const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(currentCount); - console.log("Updates onchain: " + updateAvailableValuesResult) - console.log(await randclient.getProviderAvailableValues(PROVIDER_ID)) - if (currentCount < MINIMUM_ENTRIES) { - const entriesNeeded = TARGET_ENTRIES - currentCount; - console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries to reach ${TARGET_ENTRIES}...`); - ongoingRequest = true; - let tasksTriggered = 0; - - while (tasksTriggered < entriesNeeded) { - if (ENVIRONMENT === 'cloud' && ongoingTasks.size >= MAX_OUTSTANDING_REQUESTS) { - await monitorECSTasks(); - } else if (ENVIRONMENT !== 'cloud' && ongoingContainers.size >= MAX_OUTSTANDING_REQUESTS) { - await monitorDockerContainers(); - } else { - const taskArn = await triggerVDFJobPod(); - if (taskArn) { - tasksTriggered++; - console.log(`Task triggered: ${taskArn}. Total ongoing tasks: ${ongoingTasks.size}. Total ongoing containers ${+ ongoingContainers.size}`); - } - } - } - ongoingRequest = false; - } - } catch (error) { - console.error('Error during check and fetch:', error); - ongoingRequest = false; - } -} - -// Function to clear all output requests -async function clearAllOutputRequests(client: Client): Promise { - try { - // Fetch the current open output requests - const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - console.log("Open requests fetched:", openRequests); - - if (openRequests && openRequests.activeOutputRequests) { - openRequests.activeOutputRequests.request_ids.forEach((requestId) => { - console.log(`Sending "No data" for output request ID: ${requestId}`); - - // Send "No data" as output for each request (do not await, fire and forget) - randclient.postVDFOutputAndProof(requestId, "No data", "No data").then(() => { - console.log(`"No data" sent for request ID: ${requestId}`); - }).catch((error) => { - console.error(`Error sending "No data" for request ID: ${requestId}:`, error); - }); - }); - } else { - console.log('No Output requests to clear'); - } - } catch (error) { - console.error('An error occurred while clearing output requests:', error); - } -} - - -// Function to post VDF challenge -async function fulfillRandomChallenge(client: Client, dbId: string, requestId: string, modulus: string, input: string): Promise { - if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) return; - - try { - // Assume dbId, requestId, modulus, and input are passed in as parameters - - console.log(`Received entry details - ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); - - - console.log(`Posting VDF challenge for entry ID: ${dbId}, request ID: ${requestId}`); - await randclient.postVDFChallenge(requestId, modulus, input); - console.log(`Challenge posted for request ID: ${requestId}. Waiting to post proof...`); - } catch (error) { - console.error(`Error posting VDF challenge for request ID: ${requestId}:`, error); - } -} - - -// Function to post VDF output and proof -async function fulfillRandomOutput(client: Client, requestId: string): Promise { - try { - // Fetch the output and proof from the database using the requestId - const res = await client.query('SELECT id, output, proof FROM verifiable_delay_functions WHERE request_id = $1', [requestId]); - if (!res.rowCount) { - console.error(`No entry found for request ID: ${requestId}`); - return; - } - const { id: dbId, output, proof } = res.rows[0]; - - // console.log(`Fetched entry from database for output - ID: ${dbId}, Output: ${output}, Proof: ${proof}`); - console.log(`Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}`); - // Stringify the proof if it is an array - const proofString = Array.isArray(proof) ? JSON.stringify(proof) : proof; - - console.log(`Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); - await randclient.postVDFOutputAndProof(requestId, output, proofString); - console.log(`Proof posted for request ID: ${requestId}`); - - ongoingFulfillments.delete(dbId); - } catch (error) { - console.error(`Error fulfilling random output for request ID: ${requestId}:`, error); - } -} - -// Modified polling function to fulfill open requests if they exist -async function polling(client: Client): Promise { - console.log("Starting Polling..."); - await checkAndFetchIfNeeded(client); - - try { - console.log(PROVIDER_ID); - - // Part 1: Fetch open requests - const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - console.log("Open requests fetched:", openRequests); - - if (false) { - clearAllOutputRequests(client); - } else { - - // Part 2: Fulfill Challenge requests - if (openRequests && openRequests.activeChallengeRequests) { - for (const requestId of openRequests.activeChallengeRequests.request_ids) { - console.log("Max outstanding is "+ MAX_OUTSTANDING_FULFILLMENTS) - if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) break; - try { - // Fetch modulus and input from the database - const res = await client.query('SELECT * FROM verifiable_delay_functions WHERE request_id IS NULL ORDER BY id ASC LIMIT 1'); - if (!res.rowCount) { - console.error(`No available entry found in the database.`); - return; - } - const { id: dbId, modulus, input } = res.rows[0]; - - console.log(`Fetched entry from database - ID: ${dbId}, RequestID: ${requestId}`); - // Update the database to save the requestId with the entry - await client.query('UPDATE verifiable_delay_functions SET request_id = $1 WHERE id = $2', [requestId, dbId]); - console.log(`Updated database entry with request ID: ${requestId} for entry ID: ${dbId}`); - ongoingFulfillments.add(dbId); - console.log(`Processing challenge request ID: ${requestId}`); - await fulfillRandomChallenge(client, dbId, requestId, modulus, input); - } catch (error) { - console.log(error); - } - } - } else { - console.log('No Challenge requests'); - } - - // Part 3: Fulfill Output requests - if (openRequests && openRequests.activeOutputRequests) { - for (const requestId of openRequests.activeOutputRequests.request_ids) { - console.log(`Processing output request ID: ${requestId}`); - fulfillRandomOutput(client, requestId); - } - } else { - console.log('No Output requests'); - } - - - // Part 4: Check for completed fulfillments that are no longer in the challenge or output list - const noLongerUsedIds: string[] = []; - for (const ongoingId of ongoingFulfillments) { - const challengeInProgress = openRequests.activeChallengeRequests?.request_ids.includes(ongoingId); - const outputInProgress = openRequests.activeOutputRequests?.request_ids.includes(ongoingId); - - console.log(`Checking ID: ${ongoingId}`); - console.log(` - In active challenges: ${challengeInProgress}`); - console.log(` - In active outputs: ${outputInProgress}`); - - if (!challengeInProgress && !outputInProgress) { - noLongerUsedIds.push(ongoingId); - } - } - - // Log all IDs that are no longer in use in a single line - if (noLongerUsedIds.length > 0) { - console.log(`No longer in use: ${noLongerUsedIds.join(', ')}`); - // Optionally remove them from the ongoing fulfillments list if necessary - for (const id of noLongerUsedIds) { - console.log("Removing id from list:" + id) - //await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [id]); - ongoingFulfillments.delete(id); - } - } - } - } catch (error) { - console.error('An error occurred while fetching open random requests:', error); - } -} - - - - -// Main function -async function run(): Promise { - const client = await connectWithRetry(); - await setupDatabase(client); - - setInterval(async () => { - const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); - console.log(`Periodic log - Current database size: ${res.rows[0].count}`); - }, 10000); - - setInterval(async () => { - await polling(client); - }, POLLING_INTERVAL_MS); - - process.on("SIGTERM", async () => { - console.log("SIGTERM received. Closing database connection."); - await client.end(); - process.exit(0); - }); -} - -run().catch((err) => console.error(`Error in main function: ${err}`)); +import { Client } from 'pg'; +import Docker from 'dockerode'; +import AWS from 'aws-sdk'; +import { getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" + +const RANDOM_CONFIG: RandomClientConfig = { + tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, + processId: "vgH7EXVs6-vxxilja6lkBruHlgOkyqddFVg-BVp3eJc", + wallet: JSON.parse(process.env.WALLET_JSON!), + environment: 'mainnet' +} +//const randclient: IRandomClient = RandomClient.autoConfiguration() +const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG) + +const docker = new Docker(); + +// Database configuration +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + user: process.env.DB_USER || 'myuser', + password: process.env.DB_PASSWORD || 'mypassword', + database: process.env.DB_NAME || 'mydatabase', +}; + +// Constants for configuration +const POLLING_INTERVAL_MS = 5000; +const MINIMUM_ENTRIES = 250; +const TARGET_ENTRIES = 500; +//Expected increments per second=10×0.005=0.05 +//180 times per hour +//4,320 times per day +//1,576,800 times per year +const MAX_OUTSTANDING_REQUESTS = 50; +const MAX_OUTSTANDING_FULFILLMENTS = 50; +const MAX_RETRIES = 10; +const RETRY_DELAY_MS = 10000; +const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.4'; +const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; +const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); +const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks +const ongoingContainers = new Set(); // Track container IDs of running Docker containers +const ongoingFulfillments = new Set(); // Track request IDs for ongoing fulfillments +const PROVIDER_ID = process.env.PROVIDER_ID || "0"; +let ongoingRequest = false; +let spotInterruptions = 0; +let totalProvided = 0; +// Retry logic for connecting to PostgreSQL +async function connectWithRetry(): Promise { + const client = new Client(dbConfig); + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + await client.connect(); + console.log(`Connected to PostgreSQL database (Attempt ${attempt})`); + return client; + } catch (error) { + console.error(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + } + } + throw new Error("Failed to connect to PostgreSQL after multiple attempts"); +} + +// Setup the `verifiable_delay_functions` table if not exists +async function setupDatabase(client: Client): Promise { + + // // Drop the table if it exists + // await client.query(` + // DROP TABLE IF EXISTS verifiable_delay_functions; + // `); + // console.log("'verifiable_delay_functions' table dropped."); + + await client.query(` + CREATE TABLE IF NOT EXISTS verifiable_delay_functions ( + id TEXT PRIMARY KEY, -- Define as TEXT to match UUID format + request_id TEXT, + modulus TEXT NOT NULL, + input TEXT NOT NULL, + output TEXT NOT NULL, + proof JSON NOT NULL, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + console.log("Database setup complete. 'verifiable_delay_functions' table is ready."); +} + +// Modified function to trigger VDF job pod using ECS or Docker +async function triggerVDFJobPod(): Promise { + if (ENVIRONMENT === 'cloud') { + try { + console.log("Cloud environment detected. Launching ECS task."); + const result = await ecs.runTask({ + cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', + taskDefinition: 'vdf-job', + capacityProviderStrategy: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 1 + } + ], + networkConfiguration: { + awsvpcConfiguration: { + subnets: [process.env.SUBNET_ID || 'subnet-12345678'], + securityGroups: [process.env.SECURITY_GROUP || 'sg-12345678'], + assignPublicIp: 'ENABLED' + } + }, + overrides: { + containerOverrides: [ + { + name: 'vdf_job_container', + environment: [ + { name: 'DATABASE_TYPE', value: 'postgresql' }, + { name: 'DATABASE_HOST', value: process.env.DB_HOST || 'cloud-postgres-host' }, + { name: 'DATABASE_PORT', value: process.env.DB_PORT || '5432' }, + { name: 'DATABASE_USER', value: process.env.DB_USER || 'myuser' }, + { name: 'DATABASE_PASSWORD', value: process.env.DB_PASSWORD || 'mypassword' }, + { name: 'DATABASE_NAME', value: process.env.DB_NAME || 'mydatabase' }, + ] + } + ] + }, + count: 1 + }).promise(); + + const taskArn = result.tasks?.[0]?.taskArn; + if (taskArn) { + ongoingTasks.add(taskArn); + console.log(`ECS task started successfully: ${taskArn}`); + return taskArn; + } + return null; + } catch (error: any) { + if (error?.code === 'SpotCapacityNotAvailableException') { + spotInterruptions++; + console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); + } else { + console.error("Error launching ECS task:", error); + } + return null; + } + } else { + const containerName = `vdf_job_${Date.now()}`; + console.log(`Starting Docker container with name: ${containerName}`); + const container = await docker.createContainer({ + Image: VDF_JOB_IMAGE, + Cmd: ['python', 'main.py'], + Env: [ + `DATABASE_TYPE=postgresql`, + `DATABASE_HOST=${dbConfig.host}`, + `DATABASE_PORT=${dbConfig.port.toString()}`, + `DATABASE_USER=${dbConfig.user}`, + `DATABASE_PASSWORD=${dbConfig.password}`, + `DATABASE_NAME=${dbConfig.database}`, + ], + HostConfig: { + NetworkMode: 'backend', + }, + name: containerName + }); + + await container.start(); + ongoingContainers.add(container.id); + console.log(`Docker container ${containerName} started successfully.`); + return container.id; + } +} + +// Modified function to wait for ECS tasks to complete and remove them from tracking +async function monitorECSTasks(): Promise { + if (ongoingTasks.size === 0) return; + + const describeTasksResult = await ecs.describeTasks({ + cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', + tasks: Array.from(ongoingTasks) + }).promise(); + + describeTasksResult.tasks?.forEach(task => { + // Check capacity provider name to determine if it is running Fargate or Fargate Spot + const capacityProvider = task.capacityProviderName || 'Unknown'; + + console.log(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); + + if (task.lastStatus === 'STOPPED') { + console.log(`ECS task stopped: ${task.taskArn}`); + if (task.stoppedReason) { + console.log(`Task stopped reason: ${task.stoppedReason}`); + if (task.stoppedReason.includes('Host EC2 instance termination')) { + spotInterruptions++; + console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); + } + } else { + console.log('Task stopped due to normal completion.'); + } + ongoingTasks.delete(task.taskArn as string); + } + }); +} + +// Function to wait for Docker containers to complete and remove them from tracking +async function monitorDockerContainers(): Promise { + if (ongoingContainers.size === 0) return; + + for (const containerId of ongoingContainers) { + try { + const container = docker.getContainer(containerId); + const containerInfo = await container.inspect(); + + // Check if the container is already stopped (exited) + if (containerInfo.State.Status === 'exited') { + console.log(`Docker container stopped: ${containerId}`); + + // Attempt to remove the container, handling possible errors gracefully + try { + await container.remove({ force: true }); // Force removal to avoid "in progress" errors + console.log(`Docker container removed: ${containerId}`); + ongoingContainers.delete(containerId); + } catch (removeError) { + if (isDockerError(removeError) && removeError.statusCode === 409) { + // Error 409 means removal is in progress, so skip this container for now + console.log(`Removal of container ${containerId} is already in progress. Skipping.`); + } else { + // Handle other errors that might occur during container removal + console.error(`Error removing Docker container ${containerId}:`, removeError); + } + } + } + } catch (error) { + console.error(`Error inspecting Docker container ${containerId}:`, error); + ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) + } + } +} + +// Helper function to type guard Docker errors +function isDockerError(error: unknown): error is { statusCode: number } { + return typeof error === 'object' && error !== null && 'statusCode' in error && typeof (error as any).statusCode === 'number'; +} + +// Function to process hex output for 64-bit modulus +function hexMod64Bit(expectedOutput: string): { expectedOutput64BitBase10: string } { + // Parse the hexadecimal string into a BigInt + const number = BigInt(`0x${expectedOutput}`); + + // Define the 64-bit modulus (2^64 - 1) + const modulus = BigInt("0xFFFFFFFFFFFFFFFF"); + + // Keep dividing by modulus until we get a remainder less than modulus + let remainder = number; + while (remainder >= modulus) { + remainder = remainder % modulus; + } + + // Return the remainder in base 10 + return { + expectedOutput64BitBase10: remainder.toString(), + }; +} + +// Function to prepend 0x to hex strings +function addHexPrefix(value: string): string { + return value.startsWith('0x') ? value : `0x${value}`; +} + +// Function to check if there are fewer than MINIMUM_ENTRIES and fetch until TARGET_ENTRIES +async function checkAndFetchIfNeeded(client: Client): Promise { + try { + if (ongoingRequest) return; + const res = await client.query('SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL'); + const currentCount = parseInt(res.rows[0].count, 10); // Parse the count as an integer + console.log("Total usable db entries: " + currentCount); + + // Only try to update available values if we have some entries + if (currentCount > 0) { + try { + const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(currentCount); + console.log("Updates onchain: " + updateAvailableValuesResult); + console.log(await randclient.getProviderAvailableValues(PROVIDER_ID)); + } catch (error) { + console.log("Warning: Could not update available values:", error); + } + } + + if (currentCount < MINIMUM_ENTRIES) { + const entriesNeeded = TARGET_ENTRIES - currentCount; + console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries to reach ${TARGET_ENTRIES}...`); + ongoingRequest = true; + let tasksTriggered = 0; + + while (tasksTriggered < entriesNeeded) { + if (ENVIRONMENT === 'cloud' && ongoingTasks.size >= MAX_OUTSTANDING_REQUESTS) { + await monitorECSTasks(); + } else if (ENVIRONMENT !== 'cloud' && ongoingContainers.size >= MAX_OUTSTANDING_REQUESTS) { + await monitorDockerContainers(); + } else { + const taskArn = await triggerVDFJobPod(); + if (taskArn) { + tasksTriggered++; + console.log(`Task triggered: ${taskArn}. Total ongoing tasks: ${ongoingTasks.size}. Total ongoing containers ${+ongoingContainers.size}`); + } + } + } + ongoingRequest = false; + } + } catch (error) { + console.error('Error during check and fetch:', error); + ongoingRequest = false; + } +} + +// Function to clear all output requests +async function clearAllOutputRequests(client: Client): Promise { + try { + // Fetch the current open output requests + const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + console.log("Open requests fetched:", openRequests); + + if (openRequests && openRequests.activeOutputRequests) { + openRequests.activeOutputRequests.request_ids.forEach((requestId) => { + console.log(`Sending "No data" for output request ID: ${requestId}`); + + // Send "No data" as output for each request (do not await, fire and forget) + randclient.postVDFOutputAndProof(requestId, "No data", "No data").then(() => { + console.log(`"No data" sent for request ID: ${requestId}`); + }).catch((error) => { + console.error(`Error sending "No data" for request ID: ${requestId}:`, error); + }); + }); + } else { + console.log('No Output requests to clear'); + } + } catch (error) { + console.error('An error occurred while clearing output requests:', error); + } +} + +// Function to post VDF challenge +async function fulfillRandomChallenge(client: Client, dbId: string, requestId: string, modulus: string, input: string): Promise { + if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) return; + + try { + console.log(`Received entry details - ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); + + // Add hex prefix to modulus and input + const hexModulus = addHexPrefix(modulus); + const hexInput = addHexPrefix(input); + + console.log(`Posting VDF challenge for entry ID: ${dbId}, request ID: ${requestId}`); + await randclient.postVDFChallenge(requestId, hexModulus, hexInput); + console.log(`Challenge posted for request ID: ${requestId}. Waiting to post proof...`); + } catch (error) { + console.error(`Error posting VDF challenge for request ID: ${requestId}:`, error); + } +} + +// Function to post VDF output and proof +async function fulfillRandomOutput(client: Client, requestId: string): Promise { + try { + // Fetch the output and proof from the database using the requestId + const res = await client.query('SELECT id, output, proof FROM verifiable_delay_functions WHERE request_id = $1', [requestId]); + if (!res.rowCount) { + console.error(`No entry found for request ID: ${requestId}`); + return; + } + const { id: dbId, output, proof } = res.rows[0]; + + console.log(`Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}`); + + // Process the output through hexMod64Bit + const processedOutput = hexMod64Bit(output).expectedOutput64BitBase10; + console.log(`Processed output: ${processedOutput} For request ID: ${requestId}`); + // Process the proof array - add hex prefix to each element + let processedProof = proof; + if (Array.isArray(proof)) { + processedProof = proof.map(element => addHexPrefix(element)); + } + const proofString = JSON.stringify(processedProof); + + console.log(`Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); + await randclient.postVDFOutputAndProof(requestId, processedOutput, proofString); + console.log(`Proof posted for request ID: ${requestId}`); + + ongoingFulfillments.delete(dbId); + } catch (error) { + console.error(`Error fulfilling random output for request ID: ${requestId}:`, error); + } +} + +// Modified polling function to fulfill open requests if they exist +async function polling(client: Client): Promise { + console.log("Starting Polling..."); + await checkAndFetchIfNeeded(client); + + try { + console.log(PROVIDER_ID); + + // Part 1: Fetch open requests + const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + console.log("Open requests fetched:", openRequests); + + if (false) { + clearAllOutputRequests(client); + } else { + + // Part 2: Fulfill Challenge requests + if (openRequests && openRequests.activeChallengeRequests) { + for (const requestId of openRequests.activeChallengeRequests.request_ids) { + console.log("Max outstanding is " + MAX_OUTSTANDING_FULFILLMENTS) + if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) break; + try { + // Fetch modulus and input from the database + const res = await client.query('SELECT * FROM verifiable_delay_functions WHERE request_id IS NULL ORDER BY id ASC LIMIT 1'); + if (!res.rowCount) { + console.error(`No available entry found in the database.`); + return; + } + const { id: dbId, modulus, input } = res.rows[0]; + + console.log(`Fetched entry from database - ID: ${dbId}, RequestID: ${requestId}`); + // Update the database to save the requestId with the entry + await client.query('UPDATE verifiable_delay_functions SET request_id = $1 WHERE id = $2', [requestId, dbId]); + console.log(`Updated database entry with request ID: ${requestId} for entry ID: ${dbId}`); + ongoingFulfillments.add(dbId); + console.log(`Processing challenge request ID: ${requestId}`); + await fulfillRandomChallenge(client, dbId, requestId, modulus, input); + } catch (error) { + console.log(error); + } + } + } else { + console.log('No Challenge requests'); + } + + // Part 3: Fulfill Output requests + if (openRequests && openRequests.activeOutputRequests) { + for (const requestId of openRequests.activeOutputRequests.request_ids) { + console.log(`Processing output request ID: ${requestId}`); + fulfillRandomOutput(client, requestId); + } + } else { + console.log('No Output requests'); + } + + + // Part 4: Check for completed fulfillments that are no longer in the challenge or output list + const noLongerUsedIds: string[] = []; + for (const ongoingId of ongoingFulfillments) { + const challengeInProgress = openRequests.activeChallengeRequests?.request_ids.includes(ongoingId); + const outputInProgress = openRequests.activeOutputRequests?.request_ids.includes(ongoingId); + + console.log(`Checking ID: ${ongoingId}`); + console.log(` - In active challenges: ${challengeInProgress}`); + console.log(` - In active outputs: ${outputInProgress}`); + + if (!challengeInProgress && !outputInProgress) { + noLongerUsedIds.push(ongoingId); + } + } + + // Log all IDs that are no longer in use in a single line + if (noLongerUsedIds.length > 0) { + console.log(`No longer in use: ${noLongerUsedIds.join(', ')}`); + // Optionally remove them from the ongoing fulfillments list if necessary + for (const id of noLongerUsedIds) { + console.log("Removing id from list:" + id) + //await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [id]); + ongoingFulfillments.delete(id); + } + } + } + } catch (error) { + console.error('An error occurred while fetching open random requests:', error); + } +} + +// Main function +async function run(): Promise { + const client = await connectWithRetry(); + await setupDatabase(client); + + setInterval(async () => { + const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); + console.log(`Periodic log - Current database size: ${res.rows[0].count}`); + }, 10000); + + setInterval(async () => { + await polling(client); + }, POLLING_INTERVAL_MS); + + process.on("SIGTERM", async () => { + console.log("SIGTERM received. Closing database connection."); + await client.end(); + process.exit(0); + }); +} + +run().catch((err) => console.error(`Error in main function: ${err}`)); diff --git a/requester/src/app.ts b/requester/src/app.ts index 3b71834..3e2f97c 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -11,7 +11,7 @@ const CHANCE_TO_CALL_RANDOM = 1; const RANDOM_CONFIG: RandomClientConfig = { tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, - processId: getRandomClientAutoConfiguration().processId, + processId: "vgH7EXVs6-vxxilja6lkBruHlgOkyqddFVg-BVp3eJc", wallet: JSON.parse(process.env.REQUEST_WALLET_JSON!), environment: "mainnet", }; diff --git a/verifiable-delay-function/src/protocol_constants.py b/verifiable-delay-function/src/protocol_constants.py index 1157d0c..2001cb7 100644 --- a/verifiable-delay-function/src/protocol_constants.py +++ b/verifiable-delay-function/src/protocol_constants.py @@ -1,5 +1,5 @@ # protocol_constants.py -BIT_SIZE = 1024 # RSA modulus bit size -TOTAL_SQUARINGS = 10000 # T - Total squarings for delay +BIT_SIZE = 256 # RSA modulus bit size +TOTAL_SQUARINGS = 5000 # T - Total squarings for delay NUM_SEGMENTS = 10 # Number of segments for parallel verification From 4d6ea46df7ac77e98265cf2a7ad389f2c37b00cb Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 21 Dec 2024 13:25:01 -0500 Subject: [PATCH 10/80] added --- requester/docker | 0 requester/src/app.ts | 216 ++++++++++++++++++++++++------------------- 2 files changed, 119 insertions(+), 97 deletions(-) create mode 100644 requester/docker diff --git a/requester/docker b/requester/docker new file mode 100644 index 0000000..e69de29 diff --git a/requester/src/app.ts b/requester/src/app.ts index 3e2f97c..2d61cf1 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -1,98 +1,120 @@ -import { - getRandomClientAutoConfiguration, - IRandomClient, - RandomClient, - RandomClientConfig, -} from "ao-process-clients"; - -const PROVIDER_ID = "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE"; -const RETRY_DELAY_MS = 5000; -const CHANCE_TO_CALL_RANDOM = 1; - -const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, - processId: "vgH7EXVs6-vxxilja6lkBruHlgOkyqddFVg-BVp3eJc", - wallet: JSON.parse(process.env.REQUEST_WALLET_JSON!), - environment: "mainnet", -}; - -let totalRandomCalled = 0; -let totalTimeToFulfill = 0; -let fulfilledRequests = 0; -const outstandingRequests: Set = new Set(); - -async function main() { - const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG); - - while (true) { - console.log("Running") - try { - // Roll for random chance to make a request - if (Math.random() < CHANCE_TO_CALL_RANDOM) { - console.log("Initiating random request..."); - const callbackId = `callback-${Date.now()}`; - await randclient.createRequest([PROVIDER_ID], 1, callbackId); - totalRandomCalled++; - console.log("Random request initiated. Awaiting request ID in open requests..."); - } - - // // Check open requests - // const openRequestsResponse = await randclient.getOpenRandomRequests(PROVIDER_ID); - // const openRequestIds = openRequestsResponse.activeRequests.request_ids || []; - // console.log("Open requests:", openRequestIds); - - // // Track outstanding requests - // for (const requestId of openRequestIds) { - // if (!outstandingRequests.has(requestId)) { - // console.log(`Tracking new request: ${requestId}`); - // outstandingRequests.add(requestId); - // } - // } - - // // Check the status of outstanding requests - // if (outstandingRequests.size > 0) { - // const randomRequestsResponse = await randclient.getRandomRequests(Array.from(outstandingRequests)); - // const requests = randomRequestsResponse.randomRequestResponses || []; // Adjust based on actual response structure - // console.log(randomRequestsResponse) - // console.log(requests) - - // // for (const request of requests) { - // // const requestId = request.requestId; // Adjust if property has a different name - // // if (request?.status === "fulfilled") { - // // const fulfilledTime = Date.now(); - // // const timeToFulfill = fulfilledTime - request.createdTime; // Adjust if createdTime exists - // // totalTimeToFulfill += timeToFulfill; - // // fulfilledRequests++; - // // console.log(`Request ${requestId} fulfilled. Time to fulfill: ${timeToFulfill}ms`); - // // outstandingRequests.delete(requestId); // Stop tracking fulfilled requests - // // } else { - // // console.log(`Request ${requestId} is still being processed.`); - // // } - // // } - // } - - // // Calculate and log stats - // if (fulfilledRequests > 0) { - // const avgTimeToFulfill = totalTimeToFulfill / fulfilledRequests; - - // console.log(` - // Total Random Called: ${totalRandomCalled} - // Outstanding Requests: ${outstandingRequests.size} - // Average Time to Fulfill: ${avgTimeToFulfill}ms - // `); - // } - - // Wait before next cycle - await delay(RETRY_DELAY_MS); - } catch (error) { - console.error("An error occurred:", error); - } - } -} - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -// Call the main function +import { + getRandomClientAutoConfiguration, + IRandomClient, + RandomClient, + RandomClientConfig, +} from "ao-process-clients"; + +const PROVIDER_IDS = [ + "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE", + "vgH7EXVs6-vxxilja6lkBruHlgOkyqddFVg-BVp3eJc", + "provider3id" // Replace with actual third provider ID when available +]; + +const RETRY_DELAY_MS = 5000; +const CHANCE_TO_CALL_RANDOM = 1; + +const RANDOM_CONFIG: RandomClientConfig = { + tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, + processId: "vgH7EXVs6-vxxilja6lkBruHlgOkyqddFVg-BVp3eJc", + wallet: JSON.parse(process.env.REQUEST_WALLET_JSON!), + environment: "mainnet", +}; + +let totalRandomCalled = 0; +let totalTimeToFulfill = 0; +let fulfilledRequests = 0; +const outstandingRequests: Set = new Set(); + +function getRandomProviders(): { providers: string[], count: number } { + // Randomly select how many providers we want (1-3) + const count = Math.floor(Math.random() * 3) + 1; + + // Shuffle the provider array and take the first 'count' elements + const shuffled = [...PROVIDER_IDS] + .sort(() => Math.random() - 0.5) + .slice(0, Math.min(count, PROVIDER_IDS.length)); + + return { + providers: shuffled, + count: shuffled.length + }; +} + +async function main() { + const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG); + + while (true) { + console.log("Running") + try { + // Roll for random chance to make a request + if (Math.random() < CHANCE_TO_CALL_RANDOM) { + console.log("Initiating random request..."); + const callbackId = `callback-${Date.now()}`; + const { providers, count } = getRandomProviders(); + console.log(`Selected ${count} providers:`, providers); + await randclient.createRequest(providers, count, callbackId); + totalRandomCalled++; + console.log("Random request initiated. Awaiting request ID in open requests..."); + } + + // // Check open requests + // const openRequestsResponse = await randclient.getOpenRandomRequests(PROVIDER_IDS[0]); + // const openRequestIds = openRequestsResponse.activeRequests.request_ids || []; + // console.log("Open requests:", openRequestIds); + + // // Track outstanding requests + // for (const requestId of openRequestIds) { + // if (!outstandingRequests.has(requestId)) { + // console.log(`Tracking new request: ${requestId}`); + // outstandingRequests.add(requestId); + // } + // } + + // // Check the status of outstanding requests + // if (outstandingRequests.size > 0) { + // const randomRequestsResponse = await randclient.getRandomRequests(Array.from(outstandingRequests)); + // const requests = randomRequestsResponse.randomRequestResponses || []; // Adjust based on actual response structure + // console.log(randomRequestsResponse) + // console.log(requests) + + // // for (const request of requests) { + // // const requestId = request.requestId; // Adjust if property has a different name + // // if (request?.status === "fulfilled") { + // // const fulfilledTime = Date.now(); + // // const timeToFulfill = fulfilledTime - request.createdTime; // Adjust if createdTime exists + // // totalTimeToFulfill += timeToFulfill; + // // fulfilledRequests++; + // // console.log(`Request ${requestId} fulfilled. Time to fulfill: ${timeToFulfill}ms`); + // // outstandingRequests.delete(requestId); // Stop tracking fulfilled requests + // // } else { + // // console.log(`Request ${requestId} is still being processed.`); + // // } + // // } + // } + + // // Calculate and log stats + // if (fulfilledRequests > 0) { + // const avgTimeToFulfill = totalTimeToFulfill / fulfilledRequests; + + // console.log(` + // Total Random Called: ${totalRandomCalled} + // Outstanding Requests: ${outstandingRequests.size} + // Average Time to Fulfill: ${avgTimeToFulfill}ms + // `); + // } + + // Wait before next cycle + await delay(RETRY_DELAY_MS); + } catch (error) { + console.error("An error occurred:", error); + } + } +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// Call the main function main(); \ No newline at end of file From fbde2b62e258196d0e2c02099c143956a5ae1f59 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 17 Jan 2025 15:19:56 -0500 Subject: [PATCH 11/80] finished and working --- .env.template | 23 ++ .gitignore | 2 +- 2docker-compose.yml | 81 +++++++ docker-compose.yml | 199 +++++++++++++--- orchestrator/docs/development.md | 6 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 383 +++++++++++++++++++----------- orchestrator/src/clear_outputs.ts | 61 +++++ orchestrator/src/db_config.ts | 7 + orchestrator/src/reset_db.ts | 50 ++++ orchestrator/tsconfig.json | 9 +- randwallet.json | 11 + requester/package.json | 2 +- requester/src/app.ts | 10 +- requestwall.json | 11 + 15 files changed, 664 insertions(+), 193 deletions(-) create mode 100644 .env.template create mode 100644 2docker-compose.yml create mode 100644 orchestrator/src/clear_outputs.ts create mode 100644 orchestrator/src/db_config.ts create mode 100644 orchestrator/src/reset_db.ts create mode 100644 randwallet.json create mode 100644 requestwall.json diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..b741487 --- /dev/null +++ b/.env.template @@ -0,0 +1,23 @@ +# Instance 1 +DB_USER_1=myuser1 +DB_PASSWORD_1=mypassword1 +DB_NAME_1=mydatabase1 +WALLET_JSON_1=your_wallet_json_1 +REQUEST_WALLET_JSON_1=your_request_wallet_json_1 +PROVIDER_ID_1=your_provider_id_1 + +# Instance 2 +DB_USER_2=myuser2 +DB_PASSWORD_2=mypassword2 +DB_NAME_2=mydatabase2 +WALLET_JSON_2=your_wallet_json_2 +REQUEST_WALLET_JSON_2=your_request_wallet_json_2 +PROVIDER_ID_2=your_provider_id_2 + +# Instance 3 +DB_USER_3=myuser3 +DB_PASSWORD_3=mypassword3 +DB_NAME_3=mydatabase3 +WALLET_JSON_3=your_wallet_json_3 +REQUEST_WALLET_JSON_3=your_request_wallet_json_3 +PROVIDER_ID_3=your_provider_id_3 diff --git a/.gitignore b/.gitignore index 16af4e4..8089ddd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ venv/ .env env wallet.json - +wallet*.json # Ignore distribution/build directories dist/ build/ diff --git a/2docker-compose.yml b/2docker-compose.yml new file mode 100644 index 0000000..df93961 --- /dev/null +++ b/2docker-compose.yml @@ -0,0 +1,81 @@ +version: '3.8' + +services: + postgres: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER:-myuser} + POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword} + POSTGRES_DB: ${DB_NAME:-mydatabase} + ports: + - "5432:5432" + networks: + - backend + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-mydatabase}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator: + image: randao/orchestrator:v0.1.78 + depends_on: + postgres: + condition: service_healthy + environment: + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: ${DB_USER:-myuser} + DB_PASSWORD: ${DB_PASSWORD:-mypassword} + DB_NAME: ${DB_NAME:-mydatabase} + ENVIRONMENT: local + PATH_TO_WALLET: /app/wallet.json # Path inside the container + WALLET_JSON: ${WALLET_JSON} + PROVIDER_ID: ${PROVIDER_ID} + DOCKER_NETWORK: backend # Passing the network name + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket + - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + + requester: + image: randao/requester:v0.1.13 + environment: + PATH_TO_WALLET: /app/wallet.json # Path inside the container + REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} + PROVIDER_ID: ${PROVIDER_ID} + DOCKER_NETWORK: backend # Passing the network name + networks: + - backend + volumes: + - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + + dbeaver: + image: dbeaver/cloudbeaver:23.2.0 + environment: + CB_SERVER_SERVER_PORT: 8080 + CB_SERVER_ADMIN_NAME: "admin" + CB_SERVER_ADMIN_PASSWORD: "admin123" + depends_on: + postgres: + condition: service_healthy + networks: + - backend + ports: + - "8081:8978" # Expose the DBeaver web UI + volumes: + - dbeaver-data:/opt/cloudbeaver/workspace + +networks: + backend: + name: backend # This will set the network name explicitly + driver: bridge + +volumes: + pgdata: + driver: local + dbeaver-data: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml index 40d406d..46aa88f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,81 +1,202 @@ version: '3.8' services: - postgres: + # Instance 1 + postgres1: image: postgres:13 environment: - POSTGRES_USER: ${DB_USER:-myuser} - POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword} - POSTGRES_DB: ${DB_NAME:-mydatabase} + POSTGRES_USER: ${DB_USER_1:-myuser} + POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword} + POSTGRES_DB: ${DB_NAME_1:-mydatabase} ports: - "5432:5432" networks: - - backend + - backend1 volumes: - - pgdata:/var/lib/postgresql/data + - pgdata1:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-mydatabase}"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser} -d ${DB_NAME_1:-mydatabase}"] interval: 10s timeout: 5s retries: 5 - orchestrator: - image: randao/orchestrator:v0.1.78 + orchestrator1: + image: randao/orchestrator:v0.2.10 depends_on: - postgres: + postgres1: condition: service_healthy environment: - DB_HOST: postgres + DB_HOST: postgres1 DB_PORT: 5432 - DB_USER: ${DB_USER:-myuser} - DB_PASSWORD: ${DB_PASSWORD:-mypassword} - DB_NAME: ${DB_NAME:-mydatabase} + DB_USER: ${DB_USER_1:-myuser} + DB_PASSWORD: ${DB_PASSWORD_1:-mypassword} + DB_NAME: ${DB_NAME_1:-mydatabase} ENVIRONMENT: local - PATH_TO_WALLET: /app/wallet.json # Path inside the container - WALLET_JSON: ${WALLET_JSON} - PROVIDER_ID: ${PROVIDER_ID} - DOCKER_NETWORK: backend # Passing the network name + WALLET_JSON: ${WALLET_JSON_1} + PROVIDER_ID: ${PROVIDER_ID_1} + DOCKER_NETWORK: backend1 networks: - - backend + - backend1 volumes: - - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket - - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + - /var/run/docker.sock:/var/run/docker.sock - requester: - image: randao/requester:v0.1.13 + dbeaver1: + image: dbeaver/cloudbeaver:23.2.0 environment: - PATH_TO_WALLET: /app/wallet.json # Path inside the container - REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - PROVIDER_ID: ${PROVIDER_ID} - DOCKER_NETWORK: backend # Passing the network name + CB_SERVER_SERVER_PORT: 8080 + CB_SERVER_ADMIN_NAME: "admin1" + CB_SERVER_ADMIN_PASSWORD: "admin123" + depends_on: + postgres1: + condition: service_healthy + networks: + - backend1 + ports: + - "8081:8978" + volumes: + - dbeaver-data1:/opt/cloudbeaver/workspace + + # Instance 2 + postgres2: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_2:-myuser} + POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword} + POSTGRES_DB: ${DB_NAME_2:-mydatabase} + ports: + - "5433:5432" + networks: + - backend2 + volumes: + - pgdata2:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser} -d ${DB_NAME_2:-mydatabase}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator2: + image: randao/orchestrator:v0.2.10 + depends_on: + postgres2: + condition: service_healthy + environment: + DB_HOST: postgres2 + DB_PORT: 5432 + DB_USER: ${DB_USER_2:-myuser} + DB_PASSWORD: ${DB_PASSWORD_2:-mypassword} + DB_NAME: ${DB_NAME_2:-mydatabase} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_2} + PROVIDER_ID: ${PROVIDER_ID_2} + DOCKER_NETWORK: backend2 + networks: + - backend2 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + dbeaver2: + image: dbeaver/cloudbeaver:23.2.0 + environment: + CB_SERVER_SERVER_PORT: 8080 + CB_SERVER_ADMIN_NAME: "admin2" + CB_SERVER_ADMIN_PASSWORD: "admin123" + depends_on: + postgres2: + condition: service_healthy networks: - - backend + - backend2 + ports: + - "8082:8978" volumes: - - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + - dbeaver-data2:/opt/cloudbeaver/workspace - dbeaver: + # Instance 3 + postgres3: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_3:-myuser} + POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword} + POSTGRES_DB: ${DB_NAME_3:-mydatabase} + ports: + - "5434:5432" + networks: + - backend3 + volumes: + - pgdata3:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser} -d ${DB_NAME_3:-mydatabase}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator3: + image: randao/orchestrator:v0.2.10 + depends_on: + postgres3: + condition: service_healthy + environment: + DB_HOST: postgres3 + DB_PORT: 5432 + DB_USER: ${DB_USER_3:-myuser} + DB_PASSWORD: ${DB_PASSWORD_3:-mypassword} + DB_NAME: ${DB_NAME_3:-mydatabase} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_3} + PROVIDER_ID: ${PROVIDER_ID_3} + DOCKER_NETWORK: backend3 + networks: + - backend3 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + dbeaver3: image: dbeaver/cloudbeaver:23.2.0 environment: CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin" + CB_SERVER_ADMIN_NAME: "admin3" CB_SERVER_ADMIN_PASSWORD: "admin123" depends_on: - postgres: + postgres3: condition: service_healthy networks: - - backend + - backend3 ports: - - "8081:8978" # Expose the DBeaver web UI + - "8083:8978" volumes: - - dbeaver-data:/opt/cloudbeaver/workspace + - dbeaver-data3:/opt/cloudbeaver/workspace + + requester: + image: randao/requester:v0.1.20 + environment: + REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} + + requesterfast: + image: randao/requester-fast:v0.1.0 + environment: + REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} networks: - backend: - name: backend # This will set the network name explicitly + backend1: + name: backend1 + driver: bridge + backend2: + name: backend2 + driver: bridge + backend3: + name: backend3 driver: bridge volumes: - pgdata: + pgdata1: + driver: local + pgdata2: + driver: local + pgdata3: + driver: local + dbeaver-data1: + driver: local + dbeaver-data2: driver: local - dbeaver-data: + dbeaver-data3: driver: local diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index e1a2d70..35158c0 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -2,4 +2,8 @@ To build: Save all files Run: -docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.1.10 . \ No newline at end of file +docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.1.10 . + +docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -q) + +npx ts-node src/clear_outputs.ts \ No newline at end of file diff --git a/orchestrator/package.json b/orchestrator/package.json index d1ef1d0..f5b241a 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients":"3.4.2", + "ao-process-clients":"3.5.15", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index ea542dd..fc1430e 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -2,10 +2,11 @@ import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; import { getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" +import { dbConfig } from './db_config.js'; const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, - processId: "vgH7EXVs6-vxxilja6lkBruHlgOkyqddFVg-BVp3eJc", + tokenProcessId: "7enZBOhWsyU3A5oCt8HtMNNPHSxXYJVTlOGOetR9IDw", + processId: "KbaY8P4h9wdHYKHlBSLbXN_yd-9gxUDxSgBackUxTiQ", wallet: JSON.parse(process.env.WALLET_JSON!), environment: 'mainnet' } @@ -14,14 +15,7 @@ const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG) const docker = new Docker(); -// Database configuration -const dbConfig = { - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432', 10), - user: process.env.DB_USER || 'myuser', - password: process.env.DB_PASSWORD || 'mypassword', - database: process.env.DB_NAME || 'mydatabase', -}; + // Constants for configuration const POLLING_INTERVAL_MS = 5000; @@ -42,9 +36,12 @@ const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks const ongoingContainers = new Set(); // Track container IDs of running Docker containers const ongoingFulfillments = new Set(); // Track request IDs for ongoing fulfillments const PROVIDER_ID = process.env.PROVIDER_ID || "0"; +const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; let ongoingRequest = false; let spotInterruptions = 0; let totalProvided = 0; +let PreviousTotalAvailableRandom = 0; + // Retry logic for connecting to PostgreSQL async function connectWithRetry(): Promise { const client = new Client(dbConfig); @@ -140,7 +137,7 @@ async function triggerVDFJobPod(): Promise { return null; } } else { - const containerName = `vdf_job_${Date.now()}`; + const containerName = `vdf_job_${Date.now()}_${Math.floor(Math.random() * 10000)}`; console.log(`Starting Docker container with name: ${containerName}`); const container = await docker.createContainer({ Image: VDF_JOB_IMAGE, @@ -154,7 +151,7 @@ async function triggerVDFJobPod(): Promise { `DATABASE_NAME=${dbConfig.database}`, ], HostConfig: { - NetworkMode: 'backend', + NetworkMode: DOCKER_NETWORK, }, name: containerName }); @@ -243,7 +240,7 @@ function hexMod64Bit(expectedOutput: string): { expectedOutput64BitBase10: strin const number = BigInt(`0x${expectedOutput}`); // Define the 64-bit modulus (2^64 - 1) - const modulus = BigInt("0xFFFFFFFFFFFFFFFF"); + const modulus = BigInt("0x7FFFFFFFF"); // Keep dividing by modulus until we get a remainder less than modulus let remainder = number; @@ -267,40 +264,36 @@ async function checkAndFetchIfNeeded(client: Client): Promise { try { if (ongoingRequest) return; const res = await client.query('SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL'); - const currentCount = parseInt(res.rows[0].count, 10); // Parse the count as an integer + const currentCount = parseInt(res.rows[0].count, 10); console.log("Total usable db entries: " + currentCount); - // Only try to update available values if we have some entries - if (currentCount > 0) { - try { - const updateAvailableValuesResult = await randclient.updateProviderAvailableValues(currentCount); - console.log("Updates onchain: " + updateAvailableValuesResult); - console.log(await randclient.getProviderAvailableValues(PROVIDER_ID)); - } catch (error) { - console.log("Warning: Could not update available values:", error); - } - } - if (currentCount < MINIMUM_ENTRIES) { const entriesNeeded = TARGET_ENTRIES - currentCount; console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries to reach ${TARGET_ENTRIES}...`); ongoingRequest = true; - let tasksTriggered = 0; - - while (tasksTriggered < entriesNeeded) { - if (ENVIRONMENT === 'cloud' && ongoingTasks.size >= MAX_OUTSTANDING_REQUESTS) { - await monitorECSTasks(); - } else if (ENVIRONMENT !== 'cloud' && ongoingContainers.size >= MAX_OUTSTANDING_REQUESTS) { - await monitorDockerContainers(); - } else { - const taskArn = await triggerVDFJobPod(); - if (taskArn) { - tasksTriggered++; - console.log(`Task triggered: ${taskArn}. Total ongoing tasks: ${ongoingTasks.size}. Total ongoing containers ${+ongoingContainers.size}`); - } + + const batchCount = Math.min(entriesNeeded, MAX_OUTSTANDING_REQUESTS); + console.log(`Batchcount: ${batchCount}, Ongoing containers: ${ongoingContainers.size}`); + + let spawnCount = Math.min(batchCount, MAX_OUTSTANDING_REQUESTS - ongoingContainers.size); + if (spawnCount <= 0) { + console.log("Max outstanding containers reached. Skipping new container launches."); + return; + } + + for (let i = 0; i < spawnCount; i++) { + if (ENVIRONMENT === 'cloud' && ongoingTasks.size < MAX_OUTSTANDING_REQUESTS) { + triggerVDFJobPod().then(taskArn => { + if (taskArn) console.log(`ECS task triggered: ${taskArn}`); + }).catch(console.error); + } else if (ENVIRONMENT !== 'cloud' && ongoingContainers.size < MAX_OUTSTANDING_REQUESTS) { + triggerVDFJobPod().then(containerId => { + if (containerId) console.log(`Docker container triggered: ${containerId}`); + }).catch(console.error); } } - ongoingRequest = false; + + ongoingRequest = false; // Immediately allow other operations } } catch (error) { console.error('Error during check and fetch:', error); @@ -308,51 +301,40 @@ async function checkAndFetchIfNeeded(client: Client): Promise { } } -// Function to clear all output requests -async function clearAllOutputRequests(client: Client): Promise { +// Function to post VDF challenge (fetches dbId dynamically) +async function fulfillRandomChallenge(client: Client, requestId: string): Promise { try { - // Fetch the current open output requests - const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - console.log("Open requests fetched:", openRequests); - - if (openRequests && openRequests.activeOutputRequests) { - openRequests.activeOutputRequests.request_ids.forEach((requestId) => { - console.log(`Sending "No data" for output request ID: ${requestId}`); - - // Send "No data" as output for each request (do not await, fire and forget) - randclient.postVDFOutputAndProof(requestId, "No data", "No data").then(() => { - console.log(`"No data" sent for request ID: ${requestId}`); - }).catch((error) => { - console.error(`Error sending "No data" for request ID: ${requestId}:`, error); - }); - }); - } else { - console.log('No Output requests to clear'); + // Fetch the necessary details from the database using requestId + const res = await client.query( + `SELECT id, modulus, input + FROM verifiable_delay_functions + WHERE request_id = $1`, + [requestId] + ); + + if (!res.rowCount) { + console.error(`No entry found for Request ID: ${requestId}`); + return; } - } catch (error) { - console.error('An error occurred while clearing output requests:', error); - } -} -// Function to post VDF challenge -async function fulfillRandomChallenge(client: Client, dbId: string, requestId: string, modulus: string, input: string): Promise { - if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) return; + const { id: dbId, modulus, input } = res.rows[0]; - try { - console.log(`Received entry details - ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); + console.log(`Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); // Add hex prefix to modulus and input const hexModulus = addHexPrefix(modulus); const hexInput = addHexPrefix(input); - console.log(`Posting VDF challenge for entry ID: ${dbId}, request ID: ${requestId}`); + console.log(`Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); await randclient.postVDFChallenge(requestId, hexModulus, hexInput); - console.log(`Challenge posted for request ID: ${requestId}. Waiting to post proof...`); + console.log(`Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); } catch (error) { - console.error(`Error posting VDF challenge for request ID: ${requestId}:`, error); + console.error(`Error posting VDF challenge for Request ID: ${requestId}:`, error); } } + + // Function to post VDF output and proof async function fulfillRandomOutput(client: Client, requestId: string): Promise { try { @@ -386,93 +368,199 @@ async function fulfillRandomOutput(client: Client, requestId: string): Promise { - console.log("Starting Polling..."); - await checkAndFetchIfNeeded(client); + if (pollingInProgress) { + console.log(`[SKIPPED] Polling is already in progress. Skipping this run.`); + return; // Prevent concurrent execution + } - try { - console.log(PROVIDER_ID); + pollingInProgress = true; // Mark polling as in progress + const logId = getLogId(); + console.log(`${logId} Starting Polling...`); - // Part 1: Fetch open requests + try { + console.log(`${logId} Step 1: Fetching open requests from the Randomness Client.`); const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - console.log("Open requests fetched:", openRequests); - - if (false) { - clearAllOutputRequests(client); - } else { - - // Part 2: Fulfill Challenge requests - if (openRequests && openRequests.activeChallengeRequests) { - for (const requestId of openRequests.activeChallengeRequests.request_ids) { - console.log("Max outstanding is " + MAX_OUTSTANDING_FULFILLMENTS) - if (ongoingFulfillments.size >= MAX_OUTSTANDING_FULFILLMENTS) break; - try { - // Fetch modulus and input from the database - const res = await client.query('SELECT * FROM verifiable_delay_functions WHERE request_id IS NULL ORDER BY id ASC LIMIT 1'); - if (!res.rowCount) { - console.error(`No available entry found in the database.`); - return; - } - const { id: dbId, modulus, input } = res.rows[0]; - - console.log(`Fetched entry from database - ID: ${dbId}, RequestID: ${requestId}`); - // Update the database to save the requestId with the entry - await client.query('UPDATE verifiable_delay_functions SET request_id = $1 WHERE id = $2', [requestId, dbId]); - console.log(`Updated database entry with request ID: ${requestId} for entry ID: ${dbId}`); - ongoingFulfillments.add(dbId); - console.log(`Processing challenge request ID: ${requestId}`); - await fulfillRandomChallenge(client, dbId, requestId, modulus, input); - } catch (error) { - console.log(error); - } - } - } else { - console.log('No Challenge requests'); - } + console.log(`${logId} Step 1: Open Requests: ${JSON.stringify(openRequests)}`); + console.log(openRequests); + console.log(`${logId} Step 1: Open requests fetched.`); + + // Run Step 2, 3, and 4 concurrently after Step 1 + await Promise.all([ + processChallengeRequests(client, openRequests.activeChallengeRequests, logId), + processOutputRequests(client, openRequests.activeOutputRequests, logId), + cleanupFulfilledEntries(client, openRequests, logId) + ]); + + console.log(`${logId} Polling cycle completed successfully.`); + } catch (error) { + console.error(`${logId} An error occurred during polling:`, error); + } finally { + pollingInProgress = false; // Reset flag after execution + } +} - // Part 3: Fulfill Output requests - if (openRequests && openRequests.activeOutputRequests) { - for (const requestId of openRequests.activeOutputRequests.request_ids) { - console.log(`Processing output request ID: ${requestId}`); - fulfillRandomOutput(client, requestId); - } - } else { - console.log('No Output requests'); - } +// Step 2: Process Challenge Requests (Database selection & assigning is atomic) +async function processChallengeRequests( + client: Client, + activeChallengeRequests: { request_ids: string[] } | undefined, + parentLogId: string +): Promise { + const logId = getLogId(); + console.log(`${logId} Step 2: Processing challenge requests.`); - // Part 4: Check for completed fulfillments that are no longer in the challenge or output list - const noLongerUsedIds: string[] = []; - for (const ongoingId of ongoingFulfillments) { - const challengeInProgress = openRequests.activeChallengeRequests?.request_ids.includes(ongoingId); - const outputInProgress = openRequests.activeOutputRequests?.request_ids.includes(ongoingId); + if (!activeChallengeRequests || activeChallengeRequests.request_ids.length === 0) { + console.log(`${logId} No Challenge Requests to process.`); + return; + } - console.log(`Checking ID: ${ongoingId}`); - console.log(` - In active challenges: ${challengeInProgress}`); - console.log(` - In active outputs: ${outputInProgress}`); + // Limit to MAX_OUTSTANDING_FULFILLMENTS requests + const requestIds = activeChallengeRequests.request_ids.slice(0, MAX_OUTSTANDING_FULFILLMENTS); + console.log(`${logId} Processing up to ${requestIds.length} requests.`); - if (!challengeInProgress && !outputInProgress) { - noLongerUsedIds.push(ongoingId); - } - } + try { + await client.query('BEGIN'); // Start transaction + + console.log(`${logId} Fetching existing request mappings.`); + // Fetch already assigned request_id -> dbId mappings + const existingMappingsRes = await client.query( + `SELECT request_id FROM verifiable_delay_functions + WHERE request_id = ANY($1) + FOR UPDATE SKIP LOCKED`, + [requestIds] + ); - // Log all IDs that are no longer in use in a single line - if (noLongerUsedIds.length > 0) { - console.log(`No longer in use: ${noLongerUsedIds.join(', ')}`); - // Optionally remove them from the ongoing fulfillments list if necessary - for (const id of noLongerUsedIds) { - console.log("Removing id from list:" + id) - //await client.query('DELETE FROM verifiable_delay_functions WHERE id = $1', [id]); - ongoingFulfillments.delete(id); - } - } + const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); + console.log(`${logId} Found ${existingRequestIds.size} already mapped requests.`); + + // Find only the unmapped requests (requestIds not in existingRequestIds) + const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); + console.log(`${logId} Unmapped requests: ${unmappedRequestIds.length}`); + + // Fetch available DB entries for unmapped requests + console.log(`${logId} Fetching available DB entries.`); + const dbRes = await client.query( + `SELECT id FROM verifiable_delay_functions + WHERE request_id IS NULL + ORDER BY id ASC + LIMIT $1 + FOR UPDATE SKIP LOCKED`, + [unmappedRequestIds.length] + ); + + const availableDbEntries = dbRes.rows.map(row => row.id); + console.log(`${logId} Found ${availableDbEntries.length} available DB entries.`); + + // Reduce request list if we don’t have enough DB entries + if (availableDbEntries.length < unmappedRequestIds.length) { + console.log(`${logId} Limiting requests to ${availableDbEntries.length} due to DB availability.`); + unmappedRequestIds.length = availableDbEntries.length; + } + + if (availableDbEntries.length === 0 && existingRequestIds.size === 0) { + console.log(`${logId} No available DB entries to process and no existing mappings.`); + await client.query('COMMIT'); // Commit to release locks + return; + } + + // Map unmapped requestIds to available DB entries (1:1) + for (let i = 0; i < unmappedRequestIds.length; i++) { + await client.query( + `UPDATE verifiable_delay_functions + SET request_id = $1 + WHERE id = $2`, + [unmappedRequestIds[i], availableDbEntries[i]] + ); + console.log(`${logId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); } + + await client.query('COMMIT'); // Commit all updates at once + + // Call fulfillRandomChallenge for all request IDs (existing + newly mapped) + for (const requestId of requestIds) { + fulfillRandomChallenge(client, requestId) + .catch(error => console.error(`${logId} Error fulfilling challenge for Request ID ${requestId}:`, error)); + } + + console.log(`${logId} Step 2 completed.`); } catch (error) { - console.error('An error occurred while fetching open random requests:', error); + console.error(`${logId} Error in processChallengeRequests:`, error); + await client.query('ROLLBACK'); // Rollback on failure } } + + + + +// Step 3: Process Output Requests (unchanged but with logging) +async function processOutputRequests( + client: Client, + activeOutputRequests: { request_ids: string[] } | undefined, + parentLogId: string +): Promise { + const logId = getLogId(); + console.log(`${logId} Step 3: Processing output requests.`); + + if (!activeOutputRequests || activeOutputRequests.request_ids.length === 0) { + console.log(`${logId} No Output Requests to process.`); + return; + } + + const outputPromises = activeOutputRequests.request_ids.map(async (requestId) => { + console.log(`${logId} Processing output request ID: ${requestId}`); + + // Run fulfillRandomOutput asynchronously (do not await) + fulfillRandomOutput(client, requestId) + .catch(error => console.error(`${logId} Error fulfilling output:`, error)); + }); + + await Promise.all(outputPromises); + console.log(`${logId} Step 3 completed.`); +} + +// Step 4: Remove fulfilled entries no longer in use (unchanged but with logging) +async function cleanupFulfilledEntries( + client: Client, + openRequests: any, + parentLogId: string +): Promise { + const logId = getLogId(); + console.log(`${logId} Step 4: Checking for fulfilled entries no longer in use.`); + + const noLongerUsedIds: string[] = []; + for (const ongoingId of ongoingFulfillments) { + const challengeInProgress = openRequests.activeChallengeRequests?.request_ids.includes(ongoingId); + const outputInProgress = openRequests.activeOutputRequests?.request_ids.includes(ongoingId); + + if (!challengeInProgress && !outputInProgress) { + noLongerUsedIds.push(ongoingId); + } + } + + if (noLongerUsedIds.length > 0) { + console.log(`${logId} No longer in use: ${noLongerUsedIds.join(', ')}`); + noLongerUsedIds.forEach((id) => { + ongoingFulfillments.delete(id); + console.log(`${logId} Removed ID ${id} from ongoing fulfillments.`); + }); + } else { + console.log(`${logId} No fulfilled entries to remove.`); + } + + console.log(`${logId} Step 4 completed.`); +} + + // Main function async function run(): Promise { const client = await connectWithRetry(); @@ -481,8 +569,19 @@ async function run(): Promise { setInterval(async () => { const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); console.log(`Periodic log - Current database size: ${res.rows[0].count}`); + // Check and fetch entries for the database if needed + console.log("Step 1: Checking and fetching database entries if below threshold."); + checkAndFetchIfNeeded(client).catch((error) => { + console.error("Error in checkAndFetchIfNeeded:", error); + }); + }, 10000); + setInterval(async () => { + await monitorDockerContainers(); + await monitorECSTasks(); + }, 30000); // Cleanup every 30 seconds + setInterval(async () => { await polling(client); }, POLLING_INTERVAL_MS); diff --git a/orchestrator/src/clear_outputs.ts b/orchestrator/src/clear_outputs.ts new file mode 100644 index 0000000..d7ee4c3 --- /dev/null +++ b/orchestrator/src/clear_outputs.ts @@ -0,0 +1,61 @@ +import { Client } from 'pg'; +import { getRandomClientAutoConfiguration, RandomClient, RandomClientConfig } from "ao-process-clients"; +import { dbConfig } from './db_config'; + +// Random Client Configuration +const RANDOM_CONFIG: RandomClientConfig = { + tokenProcessId: "7enZBOhWsyU3A5oCt8HtMNNPHSxXYJVTlOGOetR9IDw", + processId: "KbaY8P4h9wdHYKHlBSLbXN_yd-9gxUDxSgBackUxTiQ", + wallet: JSON.parse(process.env.WALLET_JSON!), + environment: 'mainnet' as const +}; + +const randclient = new RandomClient(RANDOM_CONFIG); +const PROVIDER_ID = process.env.PROVIDER_ID || "0"; + +// Function to connect to PostgreSQL +async function connectToDatabase() { + const client = new Client(dbConfig); + await client.connect(); + console.log("Connected to PostgreSQL database."); + return client; +} + +// Function to clear all output requests +async function clearAllOutputRequests(client: Client) { + try { + console.log("Fetching open output requests..."); + const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + + if (openRequests && openRequests.activeOutputRequests) { + console.log(`Found ${openRequests.activeOutputRequests.request_ids.length} output requests to clear.`); + + // Process each request + const clearPromises = openRequests.activeOutputRequests.request_ids.map(async (requestId: string) => { + console.log(`Sending "No data" for output request ID: ${requestId}`); + try { + await randclient.postVDFOutputAndProof(requestId, "No data", "No data"); + console.log(`"No data" successfully sent for request ID: ${requestId}`); + } catch (error) { + console.error(`Error sending "No data" for request ID: ${requestId}:`, error); + } + }); + + await Promise.all(clearPromises); + console.log("All output requests cleared."); + } else { + console.log("No output requests to clear."); + } + } catch (error) { + console.error("An error occurred while clearing output requests:", error); + } finally { + await client.end(); + console.log("Database connection closed."); + } +} + +// Run the function when the script is executed +(async () => { + const client = await connectToDatabase(); + await clearAllOutputRequests(client); +})(); diff --git a/orchestrator/src/db_config.ts b/orchestrator/src/db_config.ts new file mode 100644 index 0000000..22c50ca --- /dev/null +++ b/orchestrator/src/db_config.ts @@ -0,0 +1,7 @@ +export const dbConfig = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + user: process.env.DB_USER || 'myuser', + password: process.env.DB_PASSWORD || 'mypassword', + database: process.env.DB_NAME || 'mydatabase', +}; \ No newline at end of file diff --git a/orchestrator/src/reset_db.ts b/orchestrator/src/reset_db.ts new file mode 100644 index 0000000..ae62d50 --- /dev/null +++ b/orchestrator/src/reset_db.ts @@ -0,0 +1,50 @@ +import { Client, QueryResult } from 'pg'; +import { dbConfig } from './db_config'; + +interface TableRow { + tablename: string; +} + +// Function to connect to the database and drop all tables +async function resetDatabase(): Promise { + console.log("Connecting to PostgreSQL to reset the database..."); + + const client = new Client(dbConfig); + try { + await client.connect(); + console.log("Connected to database. Dropping all tables..."); + + // Disable foreign key constraints (important for dropping tables safely) + await client.query(`SET session_replication_role = 'replica';`); + + // Fetch all tables in the public schema + const tablesRes = await client.query(` + SELECT tablename FROM pg_tables WHERE schemaname = 'public'; + `); + + const tables = tablesRes.rows.map((row: TableRow) => row.tablename); + + if (tables.length === 0) { + console.log("No tables found in the database."); + } else { + // Drop each table + for (const table of tables) { + console.log(`Dropping table: ${table}`); + await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE;`); + } + console.log("All tables dropped successfully."); + } + + // Re-enable foreign key constraints + await client.query(`SET session_replication_role = 'origin';`); + + } catch (error) { + console.error("Error while resetting database:", error); + } finally { + await client.end(); + console.log("Database connection closed."); + } +} + +// Run the reset function +resetDatabase().catch(console.error); diff --git a/orchestrator/tsconfig.json b/orchestrator/tsconfig.json index 493e143..bf50834 100644 --- a/orchestrator/tsconfig.json +++ b/orchestrator/tsconfig.json @@ -5,12 +5,15 @@ "module": "commonjs", "target": "es6", "strict": true, - "esModuleInterop": true + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "declaration": true }, "include": [ "src/**/*.ts" - ], +, "src/db_config.ts", "src/clear_all_output_requests.ts", "src/reset_db.mjs" ], "exclude": [ "node_modules" ] -} \ No newline at end of file +} diff --git a/randwallet.json b/randwallet.json new file mode 100644 index 0000000..e68b8a3 --- /dev/null +++ b/randwallet.json @@ -0,0 +1,11 @@ +{ + "kty": "RSA", + "e": "AQAB", + "n": "yS6M-RRF4Q84Qhf8zr-DCvUMepQDmakGsj9tk1oOJpzWgL_jv3rWW8XtSZpPwQhN_tdiFqAiEgmdqiJxsqjGxez0o7WYL57P_usV-lRteuGPUQe0vaiWG2ls8-Gpi15oTLx-OSs4h8Ggbbd0X73fSK-xsZOgvfsGX7llGyZ0iTd6YQ84y2-cKBmPjdmaRxVhbtm8j4npsgCesmPDaKqJUTTiUhz5yXNfe-SPjELgRcPVGZ-sfgfw6PB5uduLy6iIeWLS_UOEYZRpi6F1wPkd4mJHX58GtJtY4gqJPaSueex-_ig1Q7UEFAjQEs-jaOaZOMZ-oBYhewa4DZwTPb1VcD5HzToPSgDuSZpBMVGYTRJ2-4scGWzk6ZWeUBpStiPe9-HMQgLo9PYh7JbptE6VP5B29YjRkCgzUk9cLCo7DdG6RJwkKlITeyOmjC1jrMecyhFhgahVYmKw8BtfkAHY2qJri06udSlVIIG0URJgecD3Ip9ug15cVQxLNCavnhlqPpeVoSoBDlguPzpLK8NoRB6LXf5C0kFDttARErJcVoRKWj_SlpfGDvbeuXrzQCeD4ijR4muJUzHmeveaNvy4BjPlwiJIMOMFJ1hy35P5QUaRxG37y46iBnYJyZtfs8xkmBboI6SNXxl8DMRlHkeitpRSF9o2JtF2N6zNG06se-M", + "d": "jseq5Vut_HyAwOelVFY2-p_Pq29A3t1HE1pQujM_t8e3tMh7KnpTh-6A6avKzoTGVgcmZkhf5c1NSGIJr3J5IB97wHQw_hsIIzNU9cTVrlBHFosRi0kKv4mi-bM-UNs_yrY8Hva9uBuDcFqzDQAEZ-HJiu3rNunhLC63wv0X2Aq3oArdlAQyH4OEjnfRNoHGFJYTbQvD_8DQ4GpNVQszSZ7uwLgvzvlC2vrrpfAQm3LQ2s6n7QpVv_xrZ6yZGoER2pR4iaZRXa-m-KIuoDYtL23wmbyTVbqq6kHwBP_LlFx0BI0kwFjH_DAE_FTA9MI0HEdDtvn96QJrvbz3Kuokp7JWWDbBFub8c3B6rn88KQRC3wOzAydB8ARcBbnPpkFKpw6fM-CCegc67qsJvwz2oBIcNZqqOD-X4wY7nc7vQ09i8zqD1oEkRdblnBCz4AgEh2ROBtm_kzAqvqDh92vnt0FphsD_BnEYyi_JJE92vscXWgseQj2CM4W5EL9EY1ijf0Nmkkb8DpCxfH6RwnI9BzNnitUvV16M1rxDZqga344vtnlEK7AvntkWRRFCqYE0EU0uBuWYBHKpbvGmnFovpDWXk6qIzSwci3SSMCk_i0GJ8IjJ-mO0x2wQQ7Pd0FkGOepzJq1DTlQakdVsX7LbiOUSs7Y1QTddG5G6PI5m_IE", + "p": "6BHvLYkIs0e6SPgTZwd8HBMChMauUsrro_rA1BtTZpij9a2ONK4Vji5C1_I-Ey_XEi889srF2-yOct8l2uBqZeDwgRf47Zxab3WCfwu2vnf4s7onwyI4UaFDSTCC7BZK11vg0P7vWDywnP4HorcqhAyiuY3If9bS-D_I_Ip8_ivP79qPu4H61Pe6G0zzPh1PiMoW3xdMeJaTU5ElMSiuFNhan_qeNc3EzXdo5udkL29j7pIczGspvoaT07wUd_RfqX3D_Fqdi23ZZRnclSz-BN9yj4J22Fe92IvOw0UM36alNkD-PRbw2c3zFDWgji445IxIDzG_8m6_BnIya2SG5Q", + "q": "3e0-wiXWs8kcf_gxPFFdgliBqa4PdajKZGg80J9MznErL6FJVrFvma4mDvhJdXyVzDyVIZIXH4oefR-RNz2nNycS8-icGQYap3KKZRNEhdD8HbCf3OL0nRffIRXRvfc0xgmLb0ysvXYpklrEPLMuqpvw3pxoSgW4_HZhQXQbkyX-xt9-u0v4DgAcxAiEwvCjee8Ul2iRxai01CesGTDMOivJU_ZpsjirVe94hIEwOW-2fjzX6zXiPJxkvtQbSwRMPzkuIYtXsj4kjf_0I0H6gqifFeVd-LHI6zWuYeoxrMWYFKBb6b3JQ_I6iS7vPgQ7z9obIE0Z7CjrUIysx_ZDJw", + "dp": "tMGzT_5aXnnR6SAAzNERpDRSU-UExsvzOmgHZa5bCaB-pM8n4nRtqa7ytYyjOQKcPDe6_mb2MdRRJ7wTmiYN-Yh5C7QGWdzcu9AFcrtG_ZgoiKTIb77pqvs2k31LnGPIq3GO7HqFJm8vCTj77YtJfEzzOh_rOVe0P1Q_UiT0Mm0hqyrLpTsaimLh_H21QH5IAr2VjvJwx8RQwFhfZajP3sCd5dmo_TNmxLrrZF56tE_IwHviHn6hpxrfbZ4jO0OGd1fUHWzfJUjMeWjpXPAMcvMwIgN2WhANeOt8gq_31QPRzy5UWHTT6HH3kZgrlFMAUVPKlLslTMlh1L2B9A_62Q", + "dq": "1ldqctda65_E7_AFla08NEVJTlm3wrr4Z0up2RDSfN0eic0r6RhMolBpn7G8OUXP5Edq_dZ8kNC0q8KOXZ0lYIZTrtGt2hlkKu_crMyUNO5oYkCR1iQ5f3Rr5CePwPr-tHrJegDDIeX7NsiFmd6xpsQgOtEzhLLMPMIVIsOCUney_98iJsGz3cnL_qX_m8wRCBaae7XafN55cCK0_Et-JHzf4UEwSpqjGMfGTav8qKy1xGz9WcZcMJAYWZrAlY6cGcAfRvSvCY8tfRyFbnwt-H3l0J8MSMNlO49IUnd_7M-XF-zdeP79YauVT6POG8a5AgI0itkMvWO0CsMjqam2pw", + "qi": "xMOtYWsparV6PZR0FqEzafuVpZLCE3Y8NR81f1ZBFUNhZVd4nGSlC5dgozfWJ--Gh9ejyb8-vvJaK2FJCUqODQ-xWQm48jADCOSOmYaM7IR7oqLJGdFWbac-IJm68_53Ql_F6OSrzZtjaphQ0dR5ax4NibE5bTx70eiAnlgdNiijZBmGlfVpMQopb3M3rOSZDtw_WBsSiduIrPIjy2FcMORpOk3849KOHb1nxCWuqWfxtywzxptRuj_RKIHmgqQ2owvJpDCyHbQrgVgqZhT7JnPAcUA7U8hMSl9I-tDlk9YhakZsMtEzQQgOERb7p7_N3d10OE-2VitpH4_Gl_u0Uw" +} \ No newline at end of file diff --git a/requester/package.json b/requester/package.json index e502c8a..eb0bca0 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "3.4.2", + "ao-process-clients": "3.5.15", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/requester/src/app.ts b/requester/src/app.ts index 2d61cf1..8f4dec8 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -7,16 +7,16 @@ import { const PROVIDER_IDS = [ "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE", - "vgH7EXVs6-vxxilja6lkBruHlgOkyqddFVg-BVp3eJc", - "provider3id" // Replace with actual third provider ID when available + "c8Iq4yunDnsJWGSz_wYwQU--O9qeODKHiRdUkQkW2p8", + "Sr3HVH0Nh6iZzbORLpoQFOEvmsuKjXsHswSWH760KAk" ]; -const RETRY_DELAY_MS = 5000; +const RETRY_DELAY_MS = 600; //1 minute const CHANCE_TO_CALL_RANDOM = 1; const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: getRandomClientAutoConfiguration().tokenProcessId, - processId: "vgH7EXVs6-vxxilja6lkBruHlgOkyqddFVg-BVp3eJc", + tokenProcessId: "7enZBOhWsyU3A5oCt8HtMNNPHSxXYJVTlOGOetR9IDw", + processId: "KbaY8P4h9wdHYKHlBSLbXN_yd-9gxUDxSgBackUxTiQ", wallet: JSON.parse(process.env.REQUEST_WALLET_JSON!), environment: "mainnet", }; diff --git a/requestwall.json b/requestwall.json new file mode 100644 index 0000000..ba122ef --- /dev/null +++ b/requestwall.json @@ -0,0 +1,11 @@ +{ + "kty": "RSA", + "e": "AQAB", + "n": "mGLG_TCdxfF13K2btXwZ0otabTAuHSmKezLAlISQGbQtgG6JiPVhEuTMiYHVSQJTXS_QuCtSuZUpIUy9A9OyQzZAea91ZWnGDXph06TSAc2ix_Ynd19YM-ilWi81T0ZtZpBkfwhHkTq3rzXdX9Bz0qK3fjmnSi1pmbK9g-kYbRh6fG-gxYvk1uTO07asX1P2ik1tnSnTByrBNEju5qN8QWTcOLKSefww_-a-AQJxB7nEAyBP5QO8ZOoemwrkfecudvKD8HmwtXIJtO2z_nC5Maa_a8diSFUpWDHDZyEDT4ReqFL_4oJmSSSlzf3awzQnDI2P1_iKCc38ir323d5U6yZ08ujXDR390tlY1UNwHnJu6ofanNHcwR206u36vAv3rIXb41ra52Y24ZpZv_4iLA1vuOxRsjhoEarpVrctOcrDqGHR4GaW28VmfGXKwGikfmE54_63GPjrJz5iQwetF6AT6z7Xhs3sgrRvOJWOne7wyaWwuesBTgHxnniR2wvGCdvANX8u5p85E47hwGVRK1pQmg3HFtNXJ_gWUpzibtRM91wRpgRVCs07xnJ1N-sjH6Wq9mAqFQb9MX3dmAtT1HMWgej5UJHOxlRBwi2Ik2gr6EfArCAI5FkXjS2UwKI5RC-Njgij_ZwB_s_Wow4lfcXV6EOdNz_pGIW8QYvjt-0", + "d": "NVguzKdeM9LpLY1gutFswLWjvCFnozHNln8Xx2X4g-b9Hr6TSuyLRO_vYhufLGWyBScd67rOxMSgl1WjvVk4SCsPuwlfEVdLy6AR6uMB2TvBRjq3aiRoXhOM_tsT0GKZHZRiec3OL9rcPmueyVHRmeT4UymflGn9U56slSbeNCKjjjgNkdn9C_UhKiACi4R7A8NDIgdoJlQjOkhlyTtl3gaOyRJKEIXmKU7_KJ9QLiIrqeR_023RX4pNmHzhq-ln7J_M7DvNTldTapDjt6iCTA1RaUts7mFRjdwtyUZXQFjYQhh8FzEK2YQIvumk0TzKhqxvtGz-9Zu0UmVZrCWOmBBPOjKFvqylWL9eP5ln3xchI-V2D7eIOgQgrZxREe7gPGwbWjdt27wIGJMilEyFx4oNFNUSABz5zUADlNOPkIfQWARf54_uf1i_6K1WFNmwlZ_exqpUUQIjadciDPtfXBIsmKiB6rC86vLZL2oZBqSCSlRanOSQCyFoiF1AHu71oK1UPf1wvI75xXIjtfvNfckl0tUJFXQ3eEMUd-3Vdsqp4ZKM8Xy0Nku3S9eTEf4NuZyEwatQUMMtf6dXa8FVnHIWgKCeq6K1GMJqm2RCp9jlz1bp72UZR8XQh7jjpJSo2LFGeWGlhHhfE0MPgrof4XPSkm6aIb9V03kTwphb4Qk", + "p": "00RB7ab0zVMjcJqAOU1EiEDQs0ZsI4yLpfFfRoofpcxdkPxlCYbgQw-hLjG2wqSM_mEKc6jkXDwl-9eb7Cu0gGQLlYikgOi29Igq0Es_ExuurIpp3GjIymQoWiHrHJzdCIxXTV5simBEsrXKzlFXSLyha5Hx6ZLgdAq7ltQ2xACZEHbegvy3mjLY-EFsOc8SomA2cao3E3-MSKClR98m3pBmaNKqiVATh-o5QbGYmedxCCWXYNgfqArEOz2WP6kxcbLes5ZeR_lqv39AH-r4UAT9GXTS60cfk693qceLtnHDKSVGpWBB7oKjf9zlHwVP_yKnAtw_ngM2hs1SXmHxWw", + "q": "uKbgliLv09MKlsuCxXMqRIRvOzz74hFB2ZJ5GO_zJX4AVFNzIMmgUs1lY2UrRqu0nnuUUF4eJXCQz3yekkV5sUpYnHBhAy7PaqRSpCyeA6ME8w6yyO_lkVIU-r-MxYthkBct8Qro0vZRsWz9WjzMjqvKPgmcVxL5x3Bzohvzse17S3dGrMLdodffFs2WKszGOin3smLgoKekrE4H0yJAFJ2y7zNr8MpT3Yxia84dwPS3oPQmvCsDox950MA-ONOoefsBu0CqGXESB3odA4cb5D8Ev4tcD_LKKN7n3EYayMUlRyLo6vzJHSe-YG_edEkL1clNKD5GMdks4dFpkEy2Vw", + "dp": "ObPu_eYX_uyyRVMtqnj8OurZFd4qMxt0GQLwLugS9Mn41Fzbi9qW7joan-9cJ5WiHOiMXkLG_JpBFaQyJjNZvaeVsmX76GmnbuqyJuomdCWfc-jqORU1onHww414FfySMn-CaNNdef4JRXZ5yhhVarx6qlLWbCF9xLQZwHx96NLw-5o79DLqQueYc1YLJU71m_wDYtBG8sBnpH6cJiFBEJEIJ6FCivagHwNOWC8VqxOXv83kpsLhApjOOA_-Na0UoCVmxk5PugDZsywia5VV9SUnrAhIg5KNgVzvpiOmeVJhJ5_Sx-CeoXJ1DbtbqTlePCVO0G0vwwzFm9QAp17A-Q", + "dq": "TguamVqi63Ej5KCX4UTP9K3VJcCc5exNXNyf-n5Q0uPy17F907nk3Zqa4-v64p_oc6PgCCf_retqCYiurTxYNyspUNXe6y151aUmmOWS1A6vJNxtjKh463c6DtGvej9zOSfS7zKhPamG1esvGljgTN0nWhlNzy_iCv3oofgWhHnPxxHe-V5Ttvg6_rReaDCtCCB85RUNxFmXLIy-meq8Evqkip29XcTmtZmEb_Rqdwf4JTuMs7OqkePX5PecVQCBES2aib7HJoG4ERj38mnEzDAmnZ8VhlgkQioU7fFjmMBbBTooIEo_5ubJfJFMAPJu4il3Ry8rFB6q7bdJWwhReQ", + "qi": "ce5PKr8QwzpPtKsZJ5Oc8i2Vj9QZO49pdBIsQKmMGLgdLv1qIR9VlraQ37n_ovdfhEqgSACOEQASgmD2EqWIwsZFtPWnz2WaJRIu0weSfu5vYNrmTPpNSQZz1F0i_TfzH-9UP-8e_yPyWhHLleicIDmfveAt32yIKu8i4mwEU_Nm4jS1B1HyMd2kQVfQ5UHpaaEHRhUhFZHk0eBUjMFnaeNGZZgB0Y_n2u_s1AkPD5TsL74wQn3joq2w3toLJLJ7_JbvMlmbnakxFz6H09o6wMjj3H8QrYlJQwQzDUlESipq1YXlIoxl0m8KHRXnPzp3Zp7sPyV1jAni_PxXOHLpXg" +} \ No newline at end of file From df776d2fc5a438404d683659feb5ea0417225d0a Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 21 Jan 2025 11:26:18 -0500 Subject: [PATCH 12/80] finished --- .gitignore | 4 +- docker-compose.yml | 130 +++++++++++++--------------------------- orchestrator/src/app.ts | 122 ++++++++++++++++++++++++++++--------- requestwall.json | 11 ---- 4 files changed, 138 insertions(+), 129 deletions(-) delete mode 100644 requestwall.json diff --git a/.gitignore b/.gitignore index 8089ddd..ca600aa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ venv/ .env/ .env env -wallet.json -wallet*.json +*wallet.json +*wallet*.json # Ignore distribution/build directories dist/ build/ diff --git a/docker-compose.yml b/docker-compose.yml index 46aa88f..155c4b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,186 +5,146 @@ services: postgres1: image: postgres:13 environment: - POSTGRES_USER: ${DB_USER_1:-myuser} - POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword} - POSTGRES_DB: ${DB_NAME_1:-mydatabase} + POSTGRES_USER: ${DB_USER_1:-myuser1} + POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword1} + POSTGRES_DB: ${DB_NAME_1:-mydatabase1} ports: - "5432:5432" networks: - - backend1 + - backend volumes: - pgdata1:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser} -d ${DB_NAME_1:-mydatabase}"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser1} -d ${DB_NAME_1:-mydatabase1}"] interval: 10s timeout: 5s retries: 5 orchestrator1: - image: randao/orchestrator:v0.2.10 + image: randao/orchestrator:v0.2.12 depends_on: postgres1: condition: service_healthy environment: DB_HOST: postgres1 DB_PORT: 5432 - DB_USER: ${DB_USER_1:-myuser} - DB_PASSWORD: ${DB_PASSWORD_1:-mypassword} - DB_NAME: ${DB_NAME_1:-mydatabase} + DB_USER: ${DB_USER_1:-myuser1} + DB_PASSWORD: ${DB_PASSWORD_1:-mypassword1} + DB_NAME: ${DB_NAME_1:-mydatabase1} ENVIRONMENT: local WALLET_JSON: ${WALLET_JSON_1} PROVIDER_ID: ${PROVIDER_ID_1} - DOCKER_NETWORK: backend1 + DOCKER_NETWORK: backend networks: - - backend1 + - backend volumes: - /var/run/docker.sock:/var/run/docker.sock - dbeaver1: - image: dbeaver/cloudbeaver:23.2.0 - environment: - CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin1" - CB_SERVER_ADMIN_PASSWORD: "admin123" - depends_on: - postgres1: - condition: service_healthy - networks: - - backend1 - ports: - - "8081:8978" - volumes: - - dbeaver-data1:/opt/cloudbeaver/workspace - # Instance 2 postgres2: image: postgres:13 environment: - POSTGRES_USER: ${DB_USER_2:-myuser} - POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword} - POSTGRES_DB: ${DB_NAME_2:-mydatabase} + POSTGRES_USER: ${DB_USER_2:-myuser2} + POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword2} + POSTGRES_DB: ${DB_NAME_2:-mydatabase2} ports: - "5433:5432" networks: - - backend2 + - backend volumes: - pgdata2:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser} -d ${DB_NAME_2:-mydatabase}"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser2} -d ${DB_NAME_2:-mydatabase2}"] interval: 10s timeout: 5s retries: 5 orchestrator2: - image: randao/orchestrator:v0.2.10 + image: randao/orchestrator:v0.2.12 depends_on: postgres2: condition: service_healthy environment: DB_HOST: postgres2 DB_PORT: 5432 - DB_USER: ${DB_USER_2:-myuser} - DB_PASSWORD: ${DB_PASSWORD_2:-mypassword} - DB_NAME: ${DB_NAME_2:-mydatabase} + DB_USER: ${DB_USER_2:-myuser2} + DB_PASSWORD: ${DB_PASSWORD_2:-mypassword2} + DB_NAME: ${DB_NAME_2:-mydatabase2} ENVIRONMENT: local WALLET_JSON: ${WALLET_JSON_2} PROVIDER_ID: ${PROVIDER_ID_2} - DOCKER_NETWORK: backend2 + DOCKER_NETWORK: backend networks: - - backend2 + - backend volumes: - /var/run/docker.sock:/var/run/docker.sock - dbeaver2: - image: dbeaver/cloudbeaver:23.2.0 - environment: - CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin2" - CB_SERVER_ADMIN_PASSWORD: "admin123" - depends_on: - postgres2: - condition: service_healthy - networks: - - backend2 - ports: - - "8082:8978" - volumes: - - dbeaver-data2:/opt/cloudbeaver/workspace - # Instance 3 postgres3: image: postgres:13 environment: - POSTGRES_USER: ${DB_USER_3:-myuser} - POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword} - POSTGRES_DB: ${DB_NAME_3:-mydatabase} + POSTGRES_USER: ${DB_USER_3:-myuser3} + POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword3} + POSTGRES_DB: ${DB_NAME_3:-mydatabase3} ports: - "5434:5432" networks: - - backend3 + - backend volumes: - pgdata3:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser} -d ${DB_NAME_3:-mydatabase}"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser3} -d ${DB_NAME_3:-mydatabase3}"] interval: 10s timeout: 5s retries: 5 orchestrator3: - image: randao/orchestrator:v0.2.10 + image: randao/orchestrator:v0.2.12 depends_on: postgres3: condition: service_healthy environment: DB_HOST: postgres3 DB_PORT: 5432 - DB_USER: ${DB_USER_3:-myuser} - DB_PASSWORD: ${DB_PASSWORD_3:-mypassword} - DB_NAME: ${DB_NAME_3:-mydatabase} + DB_USER: ${DB_USER_3:-myuser3} + DB_PASSWORD: ${DB_PASSWORD_3:-mypassword3} + DB_NAME: ${DB_NAME_3:-mydatabase3} ENVIRONMENT: local WALLET_JSON: ${WALLET_JSON_3} PROVIDER_ID: ${PROVIDER_ID_3} - DOCKER_NETWORK: backend3 + DOCKER_NETWORK: backend networks: - - backend3 + - backend volumes: - /var/run/docker.sock:/var/run/docker.sock - dbeaver3: + # Single DBeaver Instance + dbeaver: image: dbeaver/cloudbeaver:23.2.0 environment: CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin3" + CB_SERVER_ADMIN_NAME: "admin" CB_SERVER_ADMIN_PASSWORD: "admin123" - depends_on: - postgres3: - condition: service_healthy networks: - - backend3 + - backend ports: - - "8083:8978" + - "8080:8978" volumes: - - dbeaver-data3:/opt/cloudbeaver/workspace + - dbeaver-data:/opt/cloudbeaver/workspace requester: image: randao/requester:v0.1.20 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - + requesterfast: image: randao/requester-fast:v0.1.0 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} networks: - backend1: - name: backend1 - driver: bridge - backend2: - name: backend2 - driver: bridge - backend3: - name: backend3 + backend: + name: backend driver: bridge volumes: @@ -194,9 +154,5 @@ volumes: driver: local pgdata3: driver: local - dbeaver-data1: - driver: local - dbeaver-data2: - driver: local - dbeaver-data3: + dbeaver-data: driver: local diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index fc1430e..1adfb7c 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -41,6 +41,8 @@ let ongoingRequest = false; let spotInterruptions = 0; let totalProvided = 0; let PreviousTotalAvailableRandom = 0; +const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + // Retry logic for connecting to PostgreSQL async function connectWithRetry(): Promise { @@ -60,27 +62,28 @@ async function connectWithRetry(): Promise { // Setup the `verifiable_delay_functions` table if not exists async function setupDatabase(client: Client): Promise { - - // // Drop the table if it exists - // await client.query(` - // DROP TABLE IF EXISTS verifiable_delay_functions; - // `); - // console.log("'verifiable_delay_functions' table dropped."); - await client.query(` CREATE TABLE IF NOT EXISTS verifiable_delay_functions ( - id TEXT PRIMARY KEY, -- Define as TEXT to match UUID format + id TEXT PRIMARY KEY, request_id TEXT, modulus TEXT NOT NULL, input TEXT NOT NULL, output TEXT NOT NULL, proof JSON NOT NULL, - date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + detected_completed TIMESTAMP NULL ); `); + + // Ensure detected_completed column exists in case the table was created before it was added + await client.query(` + ALTER TABLE verifiable_delay_functions + ADD COLUMN IF NOT EXISTS detected_completed TIMESTAMP NULL; + `); console.log("Database setup complete. 'verifiable_delay_functions' table is ready."); } + // Modified function to trigger VDF job pod using ECS or Docker async function triggerVDFJobPod(): Promise { if (ENVIRONMENT === 'cloud') { @@ -387,20 +390,40 @@ async function polling(client: Client): Promise { console.log(`${logId} Starting Polling...`); try { + const startTime = Date.now(); // Start time of polling + console.log(`${logId} Step 1: Fetching open requests from the Randomness Client.`); + const step1Start = Date.now(); const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + const step1End = Date.now(); console.log(`${logId} Step 1: Open Requests: ${JSON.stringify(openRequests)}`); console.log(openRequests); - console.log(`${logId} Step 1: Open requests fetched.`); + console.log(`${logId} Step 1: Open requests fetched. Time taken: ${(step1End - step1Start)}ms`); // Run Step 2, 3, and 4 concurrently after Step 1 + const step2Start = Date.now(); await Promise.all([ - processChallengeRequests(client, openRequests.activeChallengeRequests, logId), - processOutputRequests(client, openRequests.activeOutputRequests, logId), - cleanupFulfilledEntries(client, openRequests, logId) + (async () => { + const s2 = Date.now(); + await processChallengeRequests(client, openRequests.activeChallengeRequests, logId); + console.log(`${logId} Step 2 completed. Time taken: ${Date.now() - s2}ms`); + })(), + (async () => { + const s3 = Date.now(); + await processOutputRequests(client, openRequests.activeOutputRequests, logId); + console.log(`${logId} Step 3 completed. Time taken: ${Date.now() - s3}ms`); + })(), + (async () => { + const s4 = Date.now(); + await cleanupFulfilledEntries(client, openRequests, logId); + console.log(`${logId} Step 4 completed. Time taken: ${Date.now() - s4}ms`); + })(), ]); + const step2End = Date.now(); + + const totalTime = step2End - startTime; + console.log(`${logId} Polling cycle completed successfully. Total time taken: ${totalTime}ms`); - console.log(`${logId} Polling cycle completed successfully.`); } catch (error) { console.error(`${logId} An error occurred during polling:`, error); } finally { @@ -409,6 +432,7 @@ async function polling(client: Client): Promise { } + // Step 2: Process Challenge Requests (Database selection & assigning is atomic) async function processChallengeRequests( client: Client, @@ -537,30 +561,70 @@ async function cleanupFulfilledEntries( const logId = getLogId(); console.log(`${logId} Step 4: Checking for fulfilled entries no longer in use.`); - const noLongerUsedIds: string[] = []; - for (const ongoingId of ongoingFulfillments) { - const challengeInProgress = openRequests.activeChallengeRequests?.request_ids.includes(ongoingId); - const outputInProgress = openRequests.activeOutputRequests?.request_ids.includes(ongoingId); + const now = new Date(); + const cutoffTime = new Date(now.getTime() - COMPLETION_RETENTION_PERIOD_MS); + + try { + await client.query('BEGIN'); + + // Fetch all entries with a request_id + const result = await client.query(` + SELECT id, request_id, detected_completed + FROM verifiable_delay_functions + WHERE request_id IS NOT NULL + `); + + let markForDeletion: string[] = []; + let markAsCompleted: string[] = []; + + for (const row of result.rows) { + const { id, request_id, detected_completed } = row; + + // Check if this request is still active in challenge or output + const isStillInChallenge = openRequests.activeChallengeRequests?.request_ids.includes(request_id); + const isStillInOutput = openRequests.activeOutputRequests?.request_ids.includes(request_id); + + if (!isStillInChallenge && !isStillInOutput) { + if (!detected_completed) { + // Mark it for deletion by setting detected_completed timestamp + markAsCompleted.push(id); + } else if (new Date(detected_completed) < cutoffTime) { + // If already marked and older than retention period, delete it + markForDeletion.push(id); + } + } + } + + // Mark entries as completed + if (markAsCompleted.length > 0) { + await client.query(` + UPDATE verifiable_delay_functions + SET detected_completed = NOW() + WHERE id = ANY($1) + `, [markAsCompleted]); + console.log(`${logId} Marked ${markAsCompleted.length} entries as completed.`); + } - if (!challengeInProgress && !outputInProgress) { - noLongerUsedIds.push(ongoingId); + // Delete old completed entries + if (markForDeletion.length > 0) { + await client.query(` + DELETE FROM verifiable_delay_functions + WHERE id = ANY($1) + `, [markForDeletion]); + console.log(`${logId} Deleted ${markForDeletion.length} old completed entries.`); } - } - if (noLongerUsedIds.length > 0) { - console.log(`${logId} No longer in use: ${noLongerUsedIds.join(', ')}`); - noLongerUsedIds.forEach((id) => { - ongoingFulfillments.delete(id); - console.log(`${logId} Removed ID ${id} from ongoing fulfillments.`); - }); - } else { - console.log(`${logId} No fulfilled entries to remove.`); + await client.query('COMMIT'); + } catch (error) { + console.error(`${logId} Error in cleanupFulfilledEntries:`, error); + await client.query('ROLLBACK'); } console.log(`${logId} Step 4 completed.`); } + // Main function async function run(): Promise { const client = await connectWithRetry(); diff --git a/requestwall.json b/requestwall.json deleted file mode 100644 index ba122ef..0000000 --- a/requestwall.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "kty": "RSA", - "e": "AQAB", - "n": "mGLG_TCdxfF13K2btXwZ0otabTAuHSmKezLAlISQGbQtgG6JiPVhEuTMiYHVSQJTXS_QuCtSuZUpIUy9A9OyQzZAea91ZWnGDXph06TSAc2ix_Ynd19YM-ilWi81T0ZtZpBkfwhHkTq3rzXdX9Bz0qK3fjmnSi1pmbK9g-kYbRh6fG-gxYvk1uTO07asX1P2ik1tnSnTByrBNEju5qN8QWTcOLKSefww_-a-AQJxB7nEAyBP5QO8ZOoemwrkfecudvKD8HmwtXIJtO2z_nC5Maa_a8diSFUpWDHDZyEDT4ReqFL_4oJmSSSlzf3awzQnDI2P1_iKCc38ir323d5U6yZ08ujXDR390tlY1UNwHnJu6ofanNHcwR206u36vAv3rIXb41ra52Y24ZpZv_4iLA1vuOxRsjhoEarpVrctOcrDqGHR4GaW28VmfGXKwGikfmE54_63GPjrJz5iQwetF6AT6z7Xhs3sgrRvOJWOne7wyaWwuesBTgHxnniR2wvGCdvANX8u5p85E47hwGVRK1pQmg3HFtNXJ_gWUpzibtRM91wRpgRVCs07xnJ1N-sjH6Wq9mAqFQb9MX3dmAtT1HMWgej5UJHOxlRBwi2Ik2gr6EfArCAI5FkXjS2UwKI5RC-Njgij_ZwB_s_Wow4lfcXV6EOdNz_pGIW8QYvjt-0", - "d": "NVguzKdeM9LpLY1gutFswLWjvCFnozHNln8Xx2X4g-b9Hr6TSuyLRO_vYhufLGWyBScd67rOxMSgl1WjvVk4SCsPuwlfEVdLy6AR6uMB2TvBRjq3aiRoXhOM_tsT0GKZHZRiec3OL9rcPmueyVHRmeT4UymflGn9U56slSbeNCKjjjgNkdn9C_UhKiACi4R7A8NDIgdoJlQjOkhlyTtl3gaOyRJKEIXmKU7_KJ9QLiIrqeR_023RX4pNmHzhq-ln7J_M7DvNTldTapDjt6iCTA1RaUts7mFRjdwtyUZXQFjYQhh8FzEK2YQIvumk0TzKhqxvtGz-9Zu0UmVZrCWOmBBPOjKFvqylWL9eP5ln3xchI-V2D7eIOgQgrZxREe7gPGwbWjdt27wIGJMilEyFx4oNFNUSABz5zUADlNOPkIfQWARf54_uf1i_6K1WFNmwlZ_exqpUUQIjadciDPtfXBIsmKiB6rC86vLZL2oZBqSCSlRanOSQCyFoiF1AHu71oK1UPf1wvI75xXIjtfvNfckl0tUJFXQ3eEMUd-3Vdsqp4ZKM8Xy0Nku3S9eTEf4NuZyEwatQUMMtf6dXa8FVnHIWgKCeq6K1GMJqm2RCp9jlz1bp72UZR8XQh7jjpJSo2LFGeWGlhHhfE0MPgrof4XPSkm6aIb9V03kTwphb4Qk", - "p": "00RB7ab0zVMjcJqAOU1EiEDQs0ZsI4yLpfFfRoofpcxdkPxlCYbgQw-hLjG2wqSM_mEKc6jkXDwl-9eb7Cu0gGQLlYikgOi29Igq0Es_ExuurIpp3GjIymQoWiHrHJzdCIxXTV5simBEsrXKzlFXSLyha5Hx6ZLgdAq7ltQ2xACZEHbegvy3mjLY-EFsOc8SomA2cao3E3-MSKClR98m3pBmaNKqiVATh-o5QbGYmedxCCWXYNgfqArEOz2WP6kxcbLes5ZeR_lqv39AH-r4UAT9GXTS60cfk693qceLtnHDKSVGpWBB7oKjf9zlHwVP_yKnAtw_ngM2hs1SXmHxWw", - "q": "uKbgliLv09MKlsuCxXMqRIRvOzz74hFB2ZJ5GO_zJX4AVFNzIMmgUs1lY2UrRqu0nnuUUF4eJXCQz3yekkV5sUpYnHBhAy7PaqRSpCyeA6ME8w6yyO_lkVIU-r-MxYthkBct8Qro0vZRsWz9WjzMjqvKPgmcVxL5x3Bzohvzse17S3dGrMLdodffFs2WKszGOin3smLgoKekrE4H0yJAFJ2y7zNr8MpT3Yxia84dwPS3oPQmvCsDox950MA-ONOoefsBu0CqGXESB3odA4cb5D8Ev4tcD_LKKN7n3EYayMUlRyLo6vzJHSe-YG_edEkL1clNKD5GMdks4dFpkEy2Vw", - "dp": "ObPu_eYX_uyyRVMtqnj8OurZFd4qMxt0GQLwLugS9Mn41Fzbi9qW7joan-9cJ5WiHOiMXkLG_JpBFaQyJjNZvaeVsmX76GmnbuqyJuomdCWfc-jqORU1onHww414FfySMn-CaNNdef4JRXZ5yhhVarx6qlLWbCF9xLQZwHx96NLw-5o79DLqQueYc1YLJU71m_wDYtBG8sBnpH6cJiFBEJEIJ6FCivagHwNOWC8VqxOXv83kpsLhApjOOA_-Na0UoCVmxk5PugDZsywia5VV9SUnrAhIg5KNgVzvpiOmeVJhJ5_Sx-CeoXJ1DbtbqTlePCVO0G0vwwzFm9QAp17A-Q", - "dq": "TguamVqi63Ej5KCX4UTP9K3VJcCc5exNXNyf-n5Q0uPy17F907nk3Zqa4-v64p_oc6PgCCf_retqCYiurTxYNyspUNXe6y151aUmmOWS1A6vJNxtjKh463c6DtGvej9zOSfS7zKhPamG1esvGljgTN0nWhlNzy_iCv3oofgWhHnPxxHe-V5Ttvg6_rReaDCtCCB85RUNxFmXLIy-meq8Evqkip29XcTmtZmEb_Rqdwf4JTuMs7OqkePX5PecVQCBES2aib7HJoG4ERj38mnEzDAmnZ8VhlgkQioU7fFjmMBbBTooIEo_5ubJfJFMAPJu4il3Ry8rFB6q7bdJWwhReQ", - "qi": "ce5PKr8QwzpPtKsZJ5Oc8i2Vj9QZO49pdBIsQKmMGLgdLv1qIR9VlraQ37n_ovdfhEqgSACOEQASgmD2EqWIwsZFtPWnz2WaJRIu0weSfu5vYNrmTPpNSQZz1F0i_TfzH-9UP-8e_yPyWhHLleicIDmfveAt32yIKu8i4mwEU_Nm4jS1B1HyMd2kQVfQ5UHpaaEHRhUhFZHk0eBUjMFnaeNGZZgB0Y_n2u_s1AkPD5TsL74wQn3joq2w3toLJLJ7_JbvMlmbnakxFz6H09o6wMjj3H8QrYlJQwQzDUlESipq1YXlIoxl0m8KHRXnPzp3Zp7sPyV1jAni_PxXOHLpXg" -} \ No newline at end of file From f3d2f46b215a8d39a9c58e95ac1d6c6cd78dd900 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 29 Jan 2025 15:07:31 -0500 Subject: [PATCH 13/80] close --- terraform/{debug_task.tf => .debug_task.tf} | 72 ++++++++++----------- terraform/backend.tf | 0 terraform/package.json | 2 +- terraform/task_definitions.tf | 4 +- 4 files changed, 39 insertions(+), 39 deletions(-) rename terraform/{debug_task.tf => .debug_task.tf} (96%) delete mode 100644 terraform/backend.tf diff --git a/terraform/debug_task.tf b/terraform/.debug_task.tf similarity index 96% rename from terraform/debug_task.tf rename to terraform/.debug_task.tf index 9972781..492af95 100644 --- a/terraform/debug_task.tf +++ b/terraform/.debug_task.tf @@ -1,36 +1,36 @@ -# debug_task.tf - -resource "aws_ecs_task_definition" "debug_task" { - family = "debug-task" - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - cpu = "512" - memory = "1024" - - container_definitions = jsonencode([ - { - name = "debug-container" - image = "amazonlinux" - essential = true - command = ["/bin/sh", "-c", "while true; do sleep 60; done"] - environment = [ - { name = "DB_HOST", value = "postgres" }, - { name = "DB_PORT", value = "5432" }, - { name = "DB_USER", value = "myuser" }, - { name = "DB_PASSWORD", value = "mypassword" }, - { name = "DB_NAME", value = "mydatabase" } - ], - logConfiguration = { - logDriver = "awslogs" - options = { - awslogs-group = "/ecs/debug-task" - awslogs-region = var.aws_region - awslogs-stream-prefix = "debug-container" - } - } - } - ]) - - execution_role_arn = aws_iam_role.execution_role.arn - task_role_arn = aws_iam_role.task_role.arn -} +# debug_task.tf + +resource "aws_ecs_task_definition" "debug_task" { + family = "debug-task" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = "512" + memory = "1024" + + container_definitions = jsonencode([ + { + name = "debug-container" + image = "amazonlinux" + essential = true + command = ["/bin/sh", "-c", "while true; do sleep 60; done"] + environment = [ + { name = "DB_HOST", value = "postgres" }, + { name = "DB_PORT", value = "5432" }, + { name = "DB_USER", value = "myuser" }, + { name = "DB_PASSWORD", value = "mypassword" }, + { name = "DB_NAME", value = "mydatabase" } + ], + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = "/ecs/debug-task" + awslogs-region = var.aws_region + awslogs-stream-prefix = "debug-container" + } + } + } + ]) + + execution_role_arn = aws_iam_role.execution_role.arn + task_role_arn = aws_iam_role.task_role.arn +} diff --git a/terraform/backend.tf b/terraform/backend.tf deleted file mode 100644 index e69de29..0000000 diff --git a/terraform/package.json b/terraform/package.json index 9f6139e..b4f6756 100644 --- a/terraform/package.json +++ b/terraform/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "ao-process-clients": "^2.3.1" + "ao-process-clients": "^2.3.17" }, "devDependencies": { "@types/node": "^22.9.1" diff --git a/terraform/task_definitions.tf b/terraform/task_definitions.tf index 7755465..ac95bbb 100644 --- a/terraform/task_definitions.tf +++ b/terraform/task_definitions.tf @@ -9,7 +9,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([{ name = "orchestrator" - image = "randao/orchestrator:v0.1.4" + image = "randao/orchestrator:v0.2.0" essential = true environment = [ { name = "ENVIRONMENT", value = "cloud" }, @@ -47,7 +47,7 @@ resource "aws_ecs_task_definition" "vdf_job" { container_definitions = jsonencode([{ name = "vdf_job_container" - image = "randao/vdf_job:v0.1.0" + image = "randao/vdf_job:v0.1.4" essential = true command = ["python", "main.py"] # Set the default command to run environment = [ From e27d744fbfef85dbc0e9494f4e40bebcbc507653 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 30 Jan 2025 09:53:45 -0500 Subject: [PATCH 14/80] Finished --- docker-compose.yml | 12 +- docs/become-a-provider.md | 85 ++++++++ orchestrator/package.json | 2 +- orchestrator/src/app.ts | 58 ++---- orchestrator/src/ecs_config.ts | 59 ++++++ randwallet.json | 11 - requester/package.json | 2 +- requester/src/app.ts | 7 +- terraform/README.md | 72 +++++++ terraform/ecs.tf | 315 +++++++++++++++++++++++++++++ terraform/ecs_cluster.tf | 16 -- terraform/ecs_service.tf | 35 ---- terraform/iam_roles.tf | 13 +- terraform/locals.tf | 23 +++ terraform/networking.tf | 30 --- terraform/outputs.tf | 17 +- terraform/posgress.tf | 24 --- terraform/providers.tf | 12 +- terraform/rds.tf | 66 ++++++ terraform/secrets.tf | 53 +++++ terraform/security.tf | 33 +++ terraform/task_definitions.tf | 73 ------- terraform/terraform.tfvars | 34 ++++ terraform/terraform.tfvars.example | 24 +++ terraform/variables.tf | 50 ++++- terraform/vpc_endpoints.tf | 116 +++++++++++ 26 files changed, 986 insertions(+), 256 deletions(-) create mode 100644 orchestrator/src/ecs_config.ts delete mode 100644 randwallet.json create mode 100644 terraform/README.md create mode 100644 terraform/ecs.tf delete mode 100644 terraform/ecs_cluster.tf delete mode 100644 terraform/ecs_service.tf create mode 100644 terraform/locals.tf delete mode 100644 terraform/networking.tf delete mode 100644 terraform/posgress.tf create mode 100644 terraform/rds.tf create mode 100644 terraform/secrets.tf create mode 100644 terraform/security.tf delete mode 100644 terraform/task_definitions.tf create mode 100644 terraform/terraform.tfvars create mode 100644 terraform/terraform.tfvars.example create mode 100644 terraform/vpc_endpoints.tf diff --git a/docker-compose.yml b/docker-compose.yml index 155c4b1..42a2d02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.2.12 + image: randao/orchestrator:v0.2.18 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.2.12 + image: randao/orchestrator:v0.2.18 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.2.12 + image: randao/orchestrator:v0.2.18 depends_on: postgres3: condition: service_healthy @@ -133,14 +133,10 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.1.20 + image: randao/requester:v0.2.0 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - requesterfast: - image: randao/requester-fast:v0.1.0 - environment: - REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} networks: backend: diff --git a/docs/become-a-provider.md b/docs/become-a-provider.md index e69de29..c7f29ad 100644 --- a/docs/become-a-provider.md +++ b/docs/become-a-provider.md @@ -0,0 +1,85 @@ +# Node Provider Setup Guide + +Welcome to the Node Provider Setup Guide for our software. This document will help you deploy and maintain a node with guaranteed 100% uptime, ensuring optimal network performance and compliance. + +## Table of Contents +1. [Introduction](#introduction) +2. [Hardware Requirements](#hardware-requirements) +3. [Deployment Options](#deployment-options) + - [Option 1: AWS Deployment with Terraform](#option-1-aws-deployment-with-terraform) + - [Option 2: Virtual Machine Deployment with Docker Compose](#option-2-virtual-machine-deployment-with-docker-compose) +4. [Graceful Shutdown Policy](#graceful-shutdown-policy) + +--- + +## Introduction +As a node provider, you are responsible for ensuring 100% uptime. In the event of downtime, it is mandatory to run the graceful shutdown script to prevent being slashed. + +This guide will help you set up and manage your node efficiently. + +--- + +## Hardware Requirements +To run a node, the following hardware specifications are required: + +- **Minimum Hardware Requirements:** + - 4 GB memory + - 2 CPU cores +- **Recommended Deployment:** Access to an AWS account (we handle the configuration) +- **Note:** These requirements will increase over time to meet network demands. + +--- + +## Deployment Options + +### Option 1: AWS Deployment with Terraform +This is the recommended method, providing the best performance and uptime at the lowest cost. + +#### Steps to Deploy: +1. **Install Terraform:** + Follow the [official Terraform installation guide](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli). + +2. **Configure AWS Environment Variables:** + Open a terminal and enter the following commands: + ```bash + export AWS_ACCESS_KEY_ID="your-access-key-id" + export AWS_SECRET_ACCESS_KEY="your-secret-access-key" + export AWS_REGION="your-region" # e.g., us-east-1 + ``` + +3. **Initialize and Apply Terraform Configuration:** + Navigate to the Terraform directory of the project and run: + ```bash + terraform init + terraform apply + ``` + Type `yes` when prompted to confirm. + +This setup ensures your node is deployed with the highest uptime and optimal performance. + +### Option 2: Virtual Machine Deployment with Docker Compose +This option may cost more and depends on the uptime of the hardware you use. It is not recommended for long-term or high-performance node operators. + +#### Steps to Deploy: +1. **Install Docker Compose:** + Follow the [official Docker Compose installation guide](https://docs.docker.com/compose/install/). + +2. **Deploy Node:** + Navigate to the Docker Compose directory and run: + ```bash + docker-compose up -d + ``` + +This setup will work but may not guarantee the same performance or reliability as the AWS-based deployment. + +--- + +## Graceful Shutdown Policy +To avoid being slashed, it is critical to run the graceful shutdown script in the event of downtime. Failing to do so may result in penalties. + +Ensure that your monitoring and alert systems are set up to notify you immediately of any issues. + +--- + +By following this guide, you can successfully deploy and maintain a node with optimal uptime and performance. Thank you for contributing to the network's success! + diff --git a/orchestrator/package.json b/orchestrator/package.json index f5b241a..4238a12 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients":"3.5.15", + "ao-process-clients":"3.5.17", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 1adfb7c..7af74d3 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -5,7 +5,7 @@ import { getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomCl import { dbConfig } from './db_config.js'; const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: "7enZBOhWsyU3A5oCt8HtMNNPHSxXYJVTlOGOetR9IDw", + tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", processId: "KbaY8P4h9wdHYKHlBSLbXN_yd-9gxUDxSgBackUxTiQ", wallet: JSON.parse(process.env.WALLET_JSON!), environment: 'mainnet' @@ -84,46 +84,25 @@ async function setupDatabase(client: Client): Promise { } +import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; + +// Cache for network configuration +let cachedNetworkConfig: NetworkConfig | null = null; + // Modified function to trigger VDF job pod using ECS or Docker async function triggerVDFJobPod(): Promise { if (ENVIRONMENT === 'cloud') { try { console.log("Cloud environment detected. Launching ECS task."); - const result = await ecs.runTask({ - cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - taskDefinition: 'vdf-job', - capacityProviderStrategy: [ - { - capacityProvider: 'FARGATE_SPOT', - weight: 1 - } - ], - networkConfiguration: { - awsvpcConfiguration: { - subnets: [process.env.SUBNET_ID || 'subnet-12345678'], - securityGroups: [process.env.SECURITY_GROUP || 'sg-12345678'], - assignPublicIp: 'ENABLED' - } - }, - overrides: { - containerOverrides: [ - { - name: 'vdf_job_container', - environment: [ - { name: 'DATABASE_TYPE', value: 'postgresql' }, - { name: 'DATABASE_HOST', value: process.env.DB_HOST || 'cloud-postgres-host' }, - { name: 'DATABASE_PORT', value: process.env.DB_PORT || '5432' }, - { name: 'DATABASE_USER', value: process.env.DB_USER || 'myuser' }, - { name: 'DATABASE_PASSWORD', value: process.env.DB_PASSWORD || 'mypassword' }, - { name: 'DATABASE_NAME', value: process.env.DB_NAME || 'mydatabase' }, - ] - } - ] - }, - count: 1 - }).promise(); - - const taskArn = result.tasks?.[0]?.taskArn; + + // Get or refresh network configuration + if (!cachedNetworkConfig) { + console.log("Fetching network configuration..."); + cachedNetworkConfig = await getNetworkConfig(ecs); + console.log("Network config:", cachedNetworkConfig); + } + + const taskArn = await launchVDFTask(ecs, cachedNetworkConfig); if (taskArn) { ongoingTasks.add(taskArn); console.log(`ECS task started successfully: ${taskArn}`); @@ -136,6 +115,8 @@ async function triggerVDFJobPod(): Promise { console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); } else { console.error("Error launching ECS task:", error); + // Reset network config cache on error to force refresh on next attempt + cachedNetworkConfig = null; } return null; } @@ -269,6 +250,11 @@ async function checkAndFetchIfNeeded(client: Client): Promise { const res = await client.query('SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL'); const currentCount = parseInt(res.rows[0].count, 10); console.log("Total usable db entries: " + currentCount); + if(PreviousTotalAvailableRandom !=currentCount){ + console.log(`Updating avalible random values from ${PreviousTotalAvailableRandom} to ${currentCount}`) + randclient.updateProviderAvailableValues(currentCount) + PreviousTotalAvailableRandom = currentCount; + } if (currentCount < MINIMUM_ENTRIES) { const entriesNeeded = TARGET_ENTRIES - currentCount; diff --git a/orchestrator/src/ecs_config.ts b/orchestrator/src/ecs_config.ts new file mode 100644 index 0000000..18f30db --- /dev/null +++ b/orchestrator/src/ecs_config.ts @@ -0,0 +1,59 @@ +import AWS from 'aws-sdk'; + +export interface NetworkConfig { + subnets: string[]; + securityGroups: string[]; +} + +export async function getNetworkConfig(ecs: AWS.ECS): Promise { + // Get the task definition to extract network configuration + const taskDef = await ecs.describeTaskDefinition({ taskDefinition: 'vdf-job' }).promise(); + + // Get the service to extract network configuration + const services = await ecs.listServices({ cluster: process.env.ECS_CLUSTER_NAME }).promise(); + const serviceArn = services.serviceArns?.find(arn => arn.includes('vdf-job-service')); + + if (!serviceArn) { + throw new Error('VDF job service not found'); + } + + const service = await ecs.describeServices({ + cluster: process.env.ECS_CLUSTER_NAME, + services: [serviceArn] + }).promise(); + + const networkConfig = service.services?.[0]?.networkConfiguration?.awsvpcConfiguration; + + if (!networkConfig?.subnets || !networkConfig?.securityGroups) { + throw new Error('Network configuration not found'); + } + + return { + subnets: networkConfig.subnets, + securityGroups: networkConfig.securityGroups + }; +} + +export async function launchVDFTask(ecs: AWS.ECS, networkConfig: NetworkConfig): Promise { + const result = await ecs.runTask({ + cluster: process.env.ECS_CLUSTER_NAME, + taskDefinition: 'vdf-job', + capacityProviderStrategy: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 1 + } + ], + networkConfiguration: { + awsvpcConfiguration: { + subnets: networkConfig.subnets, + securityGroups: networkConfig.securityGroups, + assignPublicIp: 'ENABLED' + } + }, + count: 1 + }).promise(); + + const taskArn = result.tasks?.[0]?.taskArn; + return taskArn || null; +} diff --git a/randwallet.json b/randwallet.json deleted file mode 100644 index e68b8a3..0000000 --- a/randwallet.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "kty": "RSA", - "e": "AQAB", - "n": "yS6M-RRF4Q84Qhf8zr-DCvUMepQDmakGsj9tk1oOJpzWgL_jv3rWW8XtSZpPwQhN_tdiFqAiEgmdqiJxsqjGxez0o7WYL57P_usV-lRteuGPUQe0vaiWG2ls8-Gpi15oTLx-OSs4h8Ggbbd0X73fSK-xsZOgvfsGX7llGyZ0iTd6YQ84y2-cKBmPjdmaRxVhbtm8j4npsgCesmPDaKqJUTTiUhz5yXNfe-SPjELgRcPVGZ-sfgfw6PB5uduLy6iIeWLS_UOEYZRpi6F1wPkd4mJHX58GtJtY4gqJPaSueex-_ig1Q7UEFAjQEs-jaOaZOMZ-oBYhewa4DZwTPb1VcD5HzToPSgDuSZpBMVGYTRJ2-4scGWzk6ZWeUBpStiPe9-HMQgLo9PYh7JbptE6VP5B29YjRkCgzUk9cLCo7DdG6RJwkKlITeyOmjC1jrMecyhFhgahVYmKw8BtfkAHY2qJri06udSlVIIG0URJgecD3Ip9ug15cVQxLNCavnhlqPpeVoSoBDlguPzpLK8NoRB6LXf5C0kFDttARErJcVoRKWj_SlpfGDvbeuXrzQCeD4ijR4muJUzHmeveaNvy4BjPlwiJIMOMFJ1hy35P5QUaRxG37y46iBnYJyZtfs8xkmBboI6SNXxl8DMRlHkeitpRSF9o2JtF2N6zNG06se-M", - "d": "jseq5Vut_HyAwOelVFY2-p_Pq29A3t1HE1pQujM_t8e3tMh7KnpTh-6A6avKzoTGVgcmZkhf5c1NSGIJr3J5IB97wHQw_hsIIzNU9cTVrlBHFosRi0kKv4mi-bM-UNs_yrY8Hva9uBuDcFqzDQAEZ-HJiu3rNunhLC63wv0X2Aq3oArdlAQyH4OEjnfRNoHGFJYTbQvD_8DQ4GpNVQszSZ7uwLgvzvlC2vrrpfAQm3LQ2s6n7QpVv_xrZ6yZGoER2pR4iaZRXa-m-KIuoDYtL23wmbyTVbqq6kHwBP_LlFx0BI0kwFjH_DAE_FTA9MI0HEdDtvn96QJrvbz3Kuokp7JWWDbBFub8c3B6rn88KQRC3wOzAydB8ARcBbnPpkFKpw6fM-CCegc67qsJvwz2oBIcNZqqOD-X4wY7nc7vQ09i8zqD1oEkRdblnBCz4AgEh2ROBtm_kzAqvqDh92vnt0FphsD_BnEYyi_JJE92vscXWgseQj2CM4W5EL9EY1ijf0Nmkkb8DpCxfH6RwnI9BzNnitUvV16M1rxDZqga344vtnlEK7AvntkWRRFCqYE0EU0uBuWYBHKpbvGmnFovpDWXk6qIzSwci3SSMCk_i0GJ8IjJ-mO0x2wQQ7Pd0FkGOepzJq1DTlQakdVsX7LbiOUSs7Y1QTddG5G6PI5m_IE", - "p": "6BHvLYkIs0e6SPgTZwd8HBMChMauUsrro_rA1BtTZpij9a2ONK4Vji5C1_I-Ey_XEi889srF2-yOct8l2uBqZeDwgRf47Zxab3WCfwu2vnf4s7onwyI4UaFDSTCC7BZK11vg0P7vWDywnP4HorcqhAyiuY3If9bS-D_I_Ip8_ivP79qPu4H61Pe6G0zzPh1PiMoW3xdMeJaTU5ElMSiuFNhan_qeNc3EzXdo5udkL29j7pIczGspvoaT07wUd_RfqX3D_Fqdi23ZZRnclSz-BN9yj4J22Fe92IvOw0UM36alNkD-PRbw2c3zFDWgji445IxIDzG_8m6_BnIya2SG5Q", - "q": "3e0-wiXWs8kcf_gxPFFdgliBqa4PdajKZGg80J9MznErL6FJVrFvma4mDvhJdXyVzDyVIZIXH4oefR-RNz2nNycS8-icGQYap3KKZRNEhdD8HbCf3OL0nRffIRXRvfc0xgmLb0ysvXYpklrEPLMuqpvw3pxoSgW4_HZhQXQbkyX-xt9-u0v4DgAcxAiEwvCjee8Ul2iRxai01CesGTDMOivJU_ZpsjirVe94hIEwOW-2fjzX6zXiPJxkvtQbSwRMPzkuIYtXsj4kjf_0I0H6gqifFeVd-LHI6zWuYeoxrMWYFKBb6b3JQ_I6iS7vPgQ7z9obIE0Z7CjrUIysx_ZDJw", - "dp": "tMGzT_5aXnnR6SAAzNERpDRSU-UExsvzOmgHZa5bCaB-pM8n4nRtqa7ytYyjOQKcPDe6_mb2MdRRJ7wTmiYN-Yh5C7QGWdzcu9AFcrtG_ZgoiKTIb77pqvs2k31LnGPIq3GO7HqFJm8vCTj77YtJfEzzOh_rOVe0P1Q_UiT0Mm0hqyrLpTsaimLh_H21QH5IAr2VjvJwx8RQwFhfZajP3sCd5dmo_TNmxLrrZF56tE_IwHviHn6hpxrfbZ4jO0OGd1fUHWzfJUjMeWjpXPAMcvMwIgN2WhANeOt8gq_31QPRzy5UWHTT6HH3kZgrlFMAUVPKlLslTMlh1L2B9A_62Q", - "dq": "1ldqctda65_E7_AFla08NEVJTlm3wrr4Z0up2RDSfN0eic0r6RhMolBpn7G8OUXP5Edq_dZ8kNC0q8KOXZ0lYIZTrtGt2hlkKu_crMyUNO5oYkCR1iQ5f3Rr5CePwPr-tHrJegDDIeX7NsiFmd6xpsQgOtEzhLLMPMIVIsOCUney_98iJsGz3cnL_qX_m8wRCBaae7XafN55cCK0_Et-JHzf4UEwSpqjGMfGTav8qKy1xGz9WcZcMJAYWZrAlY6cGcAfRvSvCY8tfRyFbnwt-H3l0J8MSMNlO49IUnd_7M-XF-zdeP79YauVT6POG8a5AgI0itkMvWO0CsMjqam2pw", - "qi": "xMOtYWsparV6PZR0FqEzafuVpZLCE3Y8NR81f1ZBFUNhZVd4nGSlC5dgozfWJ--Gh9ejyb8-vvJaK2FJCUqODQ-xWQm48jADCOSOmYaM7IR7oqLJGdFWbac-IJm68_53Ql_F6OSrzZtjaphQ0dR5ax4NibE5bTx70eiAnlgdNiijZBmGlfVpMQopb3M3rOSZDtw_WBsSiduIrPIjy2FcMORpOk3849KOHb1nxCWuqWfxtywzxptRuj_RKIHmgqQ2owvJpDCyHbQrgVgqZhT7JnPAcUA7U8hMSl9I-tDlk9YhakZsMtEzQQgOERb7p7_N3d10OE-2VitpH4_Gl_u0Uw" -} \ No newline at end of file diff --git a/requester/package.json b/requester/package.json index eb0bca0..798686b 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "3.5.15", + "ao-process-clients": "3.5.17", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/requester/src/app.ts b/requester/src/app.ts index 8f4dec8..99b2bd6 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -8,14 +8,15 @@ import { const PROVIDER_IDS = [ "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE", "c8Iq4yunDnsJWGSz_wYwQU--O9qeODKHiRdUkQkW2p8", - "Sr3HVH0Nh6iZzbORLpoQFOEvmsuKjXsHswSWH760KAk" + "Sr3HVH0Nh6iZzbORLpoQFOEvmsuKjXsHswSWH760KAk", + "1zlA7nKecUGevGNAEbjim_SlbioOI6daNNn2luDEHb0" ]; -const RETRY_DELAY_MS = 600; //1 minute +const RETRY_DELAY_MS = 5000; //5 seconds const CHANCE_TO_CALL_RANDOM = 1; const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: "7enZBOhWsyU3A5oCt8HtMNNPHSxXYJVTlOGOetR9IDw", + tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", processId: "KbaY8P4h9wdHYKHlBSLbXN_yd-9gxUDxSgBackUxTiQ", wallet: JSON.parse(process.env.REQUEST_WALLET_JSON!), environment: "mainnet", diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..4ba189c --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,72 @@ +# Terraform Configuration for Randomness Provider + +This Terraform configuration sets up: +- One orchestrator service on ECS Fargate +- One PostgreSQL RDS instance +- VDF Fargate spot job configuration +- AWS Secrets Manager for sensitive data + +## Setup Instructions + +1. Copy the example variables file: +```bash +cp terraform.tfvars.example terraform.tfvars +``` + +2. Edit `terraform.tfvars` with your configuration: + - Set your database credentials + - Set your provider ID + - Copy your wallet JSON and paste it into the `local_wallet_json` variable + +Example wallet JSON format: +```json +{ + "address": "your-wallet-address", + "privateKey": "your-private-key" +} +``` + +Note: The wallet JSON should be pasted directly into the terraform.tfvars file using the heredoc syntax (< Date: Thu, 30 Jan 2025 11:44:48 -0500 Subject: [PATCH 15/80] Finished --- README.md | 65 ++++ .../2docker-compose.yml | 0 docker-compose/README.md | 15 + .../docker-compose.yml | 308 +++++++++--------- docs/become-a-provider.md | 85 ----- terraform/README.md | 121 +++++++ 6 files changed, 355 insertions(+), 239 deletions(-) rename 2docker-compose.yml => docker-compose/2docker-compose.yml (100%) create mode 100644 docker-compose/README.md rename docker-compose.yml => docker-compose/docker-compose.yml (96%) delete mode 100644 docs/become-a-provider.md diff --git a/README.md b/README.md index e69de29..e0e989d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,65 @@ +# Node Provider Setup Guide + +Welcome to the Node Provider Setup Guide for our software. This document will help you deploy and maintain a node with guaranteed 100% uptime, ensuring optimal network performance and compliance. + +## Table of Contents +1. [Introduction](#introduction) +2. [Hardware Requirements](#hardware-requirements) +3. [Deployment Options](#deployment-options) + - [Option 1: AWS Deployment with Terraform](#option-1-aws-deployment-with-terraform) + - [Option 2: Virtual Machine Deployment with Docker Compose](#option-2-virtual-machine-deployment-with-docker-compose) +4. [Graceful Shutdown Policy](#graceful-shutdown-policy) + +--- + +## Introduction +As a node provider, you are responsible for ensuring 100% uptime. In the event of downtime, it is mandatory to run the graceful shutdown script to prevent being slashed. + +This guide will help you set up and manage your node efficiently. + +--- + +## Hardware Requirements +To run a node, the following hardware specifications are required: + +- **Minimum Hardware Requirements:** + - 4 GB memory + - 2 CPU cores +- **Recommended Deployment:** Access to an AWS account (we handle the configuration) +- **Note:** These requirements will increase over time to meet network demands. + +--- + +## Deployment Options + +### Option 1: AWS Deployment with Terraform +This is the recommended method, providing the best performance and uptime at the lowest cost. +[TerraForm setup](./terraform/README.md) + + + +### Option 2: Virtual Machine Deployment with Docker Compose +This option may cost more and depends on the uptime of the hardware you use. It is not recommended for long-term or high-performance node operators. +[Docker-compose setup](./docker-compose/README.md) + + +## Graceful Shutdown Policy +To avoid being slashed, it is critical to run the graceful shutdown script in the event of downtime. Failing to do so may result in penalties. + +Ensure that your monitoring and alert systems are set up to notify you immediately of any issues. + +--- + +By following this guide, you can successfully deploy and maintain a node with optimal uptime and performance. Thank you for contributing to the network's success! + +Staking + +After successfully setting up your node, you will need to provide proof that the gateway is operational. + +Show logs from the node setup to Ethan for verification. + +Upon confirmation, Ethan will provide your provider address with the necessary funds. + +3.Navigate to [Insert link here] to stake your funds and configure your provider information. + +By completing this process, you will fully activate your node and ensure it is ready for network participation. \ No newline at end of file diff --git a/2docker-compose.yml b/docker-compose/2docker-compose.yml similarity index 100% rename from 2docker-compose.yml rename to docker-compose/2docker-compose.yml diff --git a/docker-compose/README.md b/docker-compose/README.md new file mode 100644 index 0000000..0f63a1b --- /dev/null +++ b/docker-compose/README.md @@ -0,0 +1,15 @@ +#### Steps to Deploy: +1. **Install Docker Compose:** + Follow the [official Docker Compose installation guide](https://docs.docker.com/compose/install/). + +2. **Deploy Node:** + Navigate to the Docker Compose directory and run: + ```bash + docker-compose up -d + ``` + +This setup will work but may not guarantee the same performance or reliability as the AWS-based deployment. + +--- + +[Main docs](../README.md) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose/docker-compose.yml similarity index 96% rename from docker-compose.yml rename to docker-compose/docker-compose.yml index 42a2d02..32100cb 100644 --- a/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,154 +1,154 @@ -version: '3.8' - -services: - # Instance 1 - postgres1: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_1:-myuser1} - POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword1} - POSTGRES_DB: ${DB_NAME_1:-mydatabase1} - ports: - - "5432:5432" - networks: - - backend - volumes: - - pgdata1:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser1} -d ${DB_NAME_1:-mydatabase1}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator1: - image: randao/orchestrator:v0.2.18 - depends_on: - postgres1: - condition: service_healthy - environment: - DB_HOST: postgres1 - DB_PORT: 5432 - DB_USER: ${DB_USER_1:-myuser1} - DB_PASSWORD: ${DB_PASSWORD_1:-mypassword1} - DB_NAME: ${DB_NAME_1:-mydatabase1} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_1} - PROVIDER_ID: ${PROVIDER_ID_1} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 2 - postgres2: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_2:-myuser2} - POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword2} - POSTGRES_DB: ${DB_NAME_2:-mydatabase2} - ports: - - "5433:5432" - networks: - - backend - volumes: - - pgdata2:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser2} -d ${DB_NAME_2:-mydatabase2}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator2: - image: randao/orchestrator:v0.2.18 - depends_on: - postgres2: - condition: service_healthy - environment: - DB_HOST: postgres2 - DB_PORT: 5432 - DB_USER: ${DB_USER_2:-myuser2} - DB_PASSWORD: ${DB_PASSWORD_2:-mypassword2} - DB_NAME: ${DB_NAME_2:-mydatabase2} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_2} - PROVIDER_ID: ${PROVIDER_ID_2} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 3 - postgres3: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_3:-myuser3} - POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword3} - POSTGRES_DB: ${DB_NAME_3:-mydatabase3} - ports: - - "5434:5432" - networks: - - backend - volumes: - - pgdata3:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser3} -d ${DB_NAME_3:-mydatabase3}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator3: - image: randao/orchestrator:v0.2.18 - depends_on: - postgres3: - condition: service_healthy - environment: - DB_HOST: postgres3 - DB_PORT: 5432 - DB_USER: ${DB_USER_3:-myuser3} - DB_PASSWORD: ${DB_PASSWORD_3:-mypassword3} - DB_NAME: ${DB_NAME_3:-mydatabase3} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_3} - PROVIDER_ID: ${PROVIDER_ID_3} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Single DBeaver Instance - dbeaver: - image: dbeaver/cloudbeaver:23.2.0 - environment: - CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin" - CB_SERVER_ADMIN_PASSWORD: "admin123" - networks: - - backend - ports: - - "8080:8978" - volumes: - - dbeaver-data:/opt/cloudbeaver/workspace - - requester: - image: randao/requester:v0.2.0 - environment: - REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - - -networks: - backend: - name: backend - driver: bridge - -volumes: - pgdata1: - driver: local - pgdata2: - driver: local - pgdata3: - driver: local - dbeaver-data: - driver: local +version: '3.8' + +services: + # Instance 1 + postgres1: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_1:-myuser1} + POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword1} + POSTGRES_DB: ${DB_NAME_1:-mydatabase1} + ports: + - "5432:5432" + networks: + - backend + volumes: + - pgdata1:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser1} -d ${DB_NAME_1:-mydatabase1}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator1: + image: randao/orchestrator:v0.2.18 + depends_on: + postgres1: + condition: service_healthy + environment: + DB_HOST: postgres1 + DB_PORT: 5432 + DB_USER: ${DB_USER_1:-myuser1} + DB_PASSWORD: ${DB_PASSWORD_1:-mypassword1} + DB_NAME: ${DB_NAME_1:-mydatabase1} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_1} + PROVIDER_ID: ${PROVIDER_ID_1} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 2 + postgres2: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_2:-myuser2} + POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword2} + POSTGRES_DB: ${DB_NAME_2:-mydatabase2} + ports: + - "5433:5432" + networks: + - backend + volumes: + - pgdata2:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser2} -d ${DB_NAME_2:-mydatabase2}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator2: + image: randao/orchestrator:v0.2.18 + depends_on: + postgres2: + condition: service_healthy + environment: + DB_HOST: postgres2 + DB_PORT: 5432 + DB_USER: ${DB_USER_2:-myuser2} + DB_PASSWORD: ${DB_PASSWORD_2:-mypassword2} + DB_NAME: ${DB_NAME_2:-mydatabase2} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_2} + PROVIDER_ID: ${PROVIDER_ID_2} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 3 + postgres3: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_3:-myuser3} + POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword3} + POSTGRES_DB: ${DB_NAME_3:-mydatabase3} + ports: + - "5434:5432" + networks: + - backend + volumes: + - pgdata3:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser3} -d ${DB_NAME_3:-mydatabase3}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator3: + image: randao/orchestrator:v0.2.18 + depends_on: + postgres3: + condition: service_healthy + environment: + DB_HOST: postgres3 + DB_PORT: 5432 + DB_USER: ${DB_USER_3:-myuser3} + DB_PASSWORD: ${DB_PASSWORD_3:-mypassword3} + DB_NAME: ${DB_NAME_3:-mydatabase3} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_3} + PROVIDER_ID: ${PROVIDER_ID_3} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Single DBeaver Instance + dbeaver: + image: dbeaver/cloudbeaver:23.2.0 + environment: + CB_SERVER_SERVER_PORT: 8080 + CB_SERVER_ADMIN_NAME: "admin" + CB_SERVER_ADMIN_PASSWORD: "admin123" + networks: + - backend + ports: + - "8080:8978" + volumes: + - dbeaver-data:/opt/cloudbeaver/workspace + + requester: + image: randao/requester:v0.2.0 + environment: + REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} + + +networks: + backend: + name: backend + driver: bridge + +volumes: + pgdata1: + driver: local + pgdata2: + driver: local + pgdata3: + driver: local + dbeaver-data: + driver: local diff --git a/docs/become-a-provider.md b/docs/become-a-provider.md deleted file mode 100644 index c7f29ad..0000000 --- a/docs/become-a-provider.md +++ /dev/null @@ -1,85 +0,0 @@ -# Node Provider Setup Guide - -Welcome to the Node Provider Setup Guide for our software. This document will help you deploy and maintain a node with guaranteed 100% uptime, ensuring optimal network performance and compliance. - -## Table of Contents -1. [Introduction](#introduction) -2. [Hardware Requirements](#hardware-requirements) -3. [Deployment Options](#deployment-options) - - [Option 1: AWS Deployment with Terraform](#option-1-aws-deployment-with-terraform) - - [Option 2: Virtual Machine Deployment with Docker Compose](#option-2-virtual-machine-deployment-with-docker-compose) -4. [Graceful Shutdown Policy](#graceful-shutdown-policy) - ---- - -## Introduction -As a node provider, you are responsible for ensuring 100% uptime. In the event of downtime, it is mandatory to run the graceful shutdown script to prevent being slashed. - -This guide will help you set up and manage your node efficiently. - ---- - -## Hardware Requirements -To run a node, the following hardware specifications are required: - -- **Minimum Hardware Requirements:** - - 4 GB memory - - 2 CPU cores -- **Recommended Deployment:** Access to an AWS account (we handle the configuration) -- **Note:** These requirements will increase over time to meet network demands. - ---- - -## Deployment Options - -### Option 1: AWS Deployment with Terraform -This is the recommended method, providing the best performance and uptime at the lowest cost. - -#### Steps to Deploy: -1. **Install Terraform:** - Follow the [official Terraform installation guide](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli). - -2. **Configure AWS Environment Variables:** - Open a terminal and enter the following commands: - ```bash - export AWS_ACCESS_KEY_ID="your-access-key-id" - export AWS_SECRET_ACCESS_KEY="your-secret-access-key" - export AWS_REGION="your-region" # e.g., us-east-1 - ``` - -3. **Initialize and Apply Terraform Configuration:** - Navigate to the Terraform directory of the project and run: - ```bash - terraform init - terraform apply - ``` - Type `yes` when prompted to confirm. - -This setup ensures your node is deployed with the highest uptime and optimal performance. - -### Option 2: Virtual Machine Deployment with Docker Compose -This option may cost more and depends on the uptime of the hardware you use. It is not recommended for long-term or high-performance node operators. - -#### Steps to Deploy: -1. **Install Docker Compose:** - Follow the [official Docker Compose installation guide](https://docs.docker.com/compose/install/). - -2. **Deploy Node:** - Navigate to the Docker Compose directory and run: - ```bash - docker-compose up -d - ``` - -This setup will work but may not guarantee the same performance or reliability as the AWS-based deployment. - ---- - -## Graceful Shutdown Policy -To avoid being slashed, it is critical to run the graceful shutdown script in the event of downtime. Failing to do so may result in penalties. - -Ensure that your monitoring and alert systems are set up to notify you immediately of any issues. - ---- - -By following this guide, you can successfully deploy and maintain a node with optimal uptime and performance. Thank you for contributing to the network's success! - diff --git a/terraform/README.md b/terraform/README.md index 4ba189c..b999b0d 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -1,3 +1,121 @@ +#### Steps to Deploy: +1. **Install Terraform:** + Follow the [official Terraform installation guide](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli). + +2. **Log into AWS and set up IAM user and Policy** + Go To IAM and create a new policy called RandAO-Provider-Admin + Paste this in the JSON + ```{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ecs:CreateCluster", + "ecs:DeleteCluster", + "ecs:CreateService", + "ecs:DeleteService", + "ecs:UpdateService", + "ecs:RegisterTaskDefinition", + "ecs:DeregisterTaskDefinition", + "ecs:ListTaskDefinitions", + "ecs:DescribeTaskDefinition", + "ecs:PutClusterCapacityProviders" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "iam:CreateRole", + "iam:DeleteRole", + "iam:GetRole", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PassRole" + ], + "Resource": "arn:aws:iam::*:role/orchestrator-*" + }, + { + "Effect": "Allow", + "Action": [ + "secretsmanager:CreateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:GetSecretValue", + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecret", + "secretsmanager:TagResource" + ], + "Resource": "arn:aws:secretsmanager:*:*:secret:/orchestrator/*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:CreateDBInstance", + "rds:DeleteDBInstance", + "rds:ModifyDBInstance", + "rds:DescribeDBInstances", + "rds:CreateDBSubnetGroup", + "rds:DeleteDBSubnetGroup", + "rds:ModifyDBSubnetGroup" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:PutRetentionPolicy" + ], + "Resource": "arn:aws:logs:*:*:log-group:/ecs/*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateSecurityGroup", + "ec2:DeleteSecurityGroup", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress", + "ec2:CreateVpcEndpoint", + "ec2:DeleteVpcEndpoints", + "ec2:DescribeVpcEndpoints", + "ec2:DescribeSecurityGroups", + "ec2:DescribeNetworkInterfaces" + ], + "Resource": "*" + } + ] +}``` + Save the JSON and name it + Click on Users and create a new user called TeraformDeployer + Attach this new policy directly + Save the User + Click on the user and go to the Security Credentials tab + Create an access key and choose CLI + Save the variables for the next step + +3. **Configure AWS Environment Variables:** + Open a terminal and enter the following commands: + ```bash + export AWS_ACCESS_KEY_ID="your-access-key-id" + export AWS_SECRET_ACCESS_KEY="your-secret-access-key" + export AWS_REGION="your-region" # e.g., us-east-1 + ``` + +3. **Initialize and Apply Terraform Configuration:** + Navigate to the Terraform directory of the project and run: + ```bash + terraform init + terraform apply + ``` + Type `yes` when prompted to confirm. + +This setup ensures your node is deployed with the highest uptime and optimal performance. + + # Terraform Configuration for Randomness Provider This Terraform configuration sets up: @@ -70,3 +188,6 @@ Optional variables (with defaults): - `aws_region`: AWS region (default: us-east-1) - `db_name`: Database name (default: orchestrator_db) - `secrets_prefix`: Prefix for secrets (default: /orchestrator) + + +[Main docs](../README.md) \ No newline at end of file From 339f0eadf43745e8f84c5253d25254201c3fc87c Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 30 Jan 2025 11:49:46 -0500 Subject: [PATCH 16/80] Finished --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e0e989d..f377ac3 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,14 @@ To run a node, the following hardware specifications are required: ### Option 1: AWS Deployment with Terraform This is the recommended method, providing the best performance and uptime at the lowest cost. + [TerraForm setup](./terraform/README.md) ### Option 2: Virtual Machine Deployment with Docker Compose This option may cost more and depends on the uptime of the hardware you use. It is not recommended for long-term or high-performance node operators. + [Docker-compose setup](./docker-compose/README.md) From a2ec06b1e7f36e2a8fcc27a5eb6beecfdb866f41 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 30 Jan 2025 11:51:34 -0500 Subject: [PATCH 17/80] Finished --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f377ac3..9e149cc 100644 --- a/README.md +++ b/README.md @@ -50,18 +50,20 @@ To avoid being slashed, it is critical to run the graceful shutdown script in th Ensure that your monitoring and alert systems are set up to notify you immediately of any issues. +TODO + --- -By following this guide, you can successfully deploy and maintain a node with optimal uptime and performance. Thank you for contributing to the network's success! -Staking +## Staking After successfully setting up your node, you will need to provide proof that the gateway is operational. -Show logs from the node setup to Ethan for verification. +1. Show logs from the node setup to Ethan for verification. -Upon confirmation, Ethan will provide your provider address with the necessary funds. +2. Upon confirmation, Ethan will provide your provider address with the necessary funds. -3.Navigate to [Insert link here] to stake your funds and configure your provider information. +3. Navigate to [Insert link here] to stake your funds and configure your provider information. -By completing this process, you will fully activate your node and ensure it is ready for network participation. \ No newline at end of file +By completing this process, you will fully activate your node and ensure it is ready for network participation. +Thank you for contributing to the network's success! \ No newline at end of file From f61d89aa607dd6b83df0e306850f24462a515b00 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 30 Jan 2025 14:11:53 -0500 Subject: [PATCH 18/80] Finished --- README.md | 26 ++++++++++++++++++------ docker-compose/.env.example | 16 +++++++++++++++ docker-compose/2docker-compose.yml | 32 +----------------------------- docker-compose/README.md | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 docker-compose/.env.example diff --git a/README.md b/README.md index 9e149cc..688ee9b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Node Provider Setup Guide -Welcome to the Node Provider Setup Guide for our software. This document will help you deploy and maintain a node with guaranteed 100% uptime, ensuring optimal network performance and compliance. +This guide will walk you through getting your randomness provider set up and connected to the network so you can start contributing to the protocal and participating in decentralized randomness! ## Table of Contents 1. [Introduction](#introduction) @@ -15,7 +15,14 @@ Welcome to the Node Provider Setup Guide for our software. This document will he ## Introduction As a node provider, you are responsible for ensuring 100% uptime. In the event of downtime, it is mandatory to run the graceful shutdown script to prevent being slashed. -This guide will help you set up and manage your node efficiently. +Your provider does 3 main things. +1. It updates the amount of avalible random it has stored on chain. Each random takes a fair biot of compute to create so that it can be compliant to our commit reveal time delay scheme. +2. It detects someone has requested random from you and it provides the input number to your time delay function. This is not the final random number. +3. It detects all parties have submited their input number and then it provides the output number as well as the proof of history checkpoints to the chain to be verified. This output is the random number that will be used on chain. + +It will do these three steps as fast as it can inorder to get the complete random on chain as quickly as possible. Faster providers will be incentivised for their speed and slower ones will been penalized. If a provider is too slow for step 2 it will be slashed a small amount. If a provider is too slow for step 3 they will be considered malicious and slashed heavily. + +In the event you need to take your provider offline you must run the gracefull shutdown which will run step 1 once with a value of -1 indicating you are no longer offering random. After that your node will finish all requests in step 2 and 3 then turn off. --- @@ -30,6 +37,12 @@ To run a node, the following hardware specifications are required: --- + +## What the hardware runs +The hardware you stand up runs 3 services. It stands up a provider. A database for the provider and it spins up temporary jobs to generate random and store it in the database. +In AWS the cheapest and highest performance solution is used for each of these. when running with docekr compose your machinbe will run each of these services itself in a containerized environment. Quick and scalable but not as cheap or efficient as the AWS solution. + + ## Deployment Options ### Option 1: AWS Deployment with Terraform @@ -38,7 +51,6 @@ This is the recommended method, providing the best performance and uptime at the [TerraForm setup](./terraform/README.md) - ### Option 2: Virtual Machine Deployment with Docker Compose This option may cost more and depends on the uptime of the hardware you use. It is not recommended for long-term or high-performance node operators. @@ -46,11 +58,13 @@ This option may cost more and depends on the uptime of the hardware you use. It ## Graceful Shutdown Policy -To avoid being slashed, it is critical to run the graceful shutdown script in the event of downtime. Failing to do so may result in penalties. +To avoid being slashed, it is critical to run the graceful shutdown in the event of downtime. Failing to do so may result in penalties. -Ensure that your monitoring and alert systems are set up to notify you immediately of any issues. +To run this go to "TODO" and navigate to your node and select the "SHUT DOWN" button and sign the transaction. +This will tell your provider to stop serving random. -TODO +After the maintinance is done and your provider is back up again click "START UP" button. +This will tell your provider to start serving random. --- diff --git a/docker-compose/.env.example b/docker-compose/.env.example new file mode 100644 index 0000000..80d1ef5 --- /dev/null +++ b/docker-compose/.env.example @@ -0,0 +1,16 @@ +DB_USER=myuser +DB_PASSWORD=mypassword +DB_NAME=mydatabase +DOCKER_NETWORK=backend +WALLET_JSON = '{ + "kty": "RSA", + "e": "test", + "n": "test", + "d": "test", + "p": "test", + "q": "test", + "dp": "test", + "dq": "test", + "qi": "test" +}' +PROVIDER_ID = "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE" diff --git a/docker-compose/2docker-compose.yml b/docker-compose/2docker-compose.yml index df93961..d48a879 100644 --- a/docker-compose/2docker-compose.yml +++ b/docker-compose/2docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.1.78 + image: randao/orchestrator:v0.2.18 depends_on: postgres: condition: service_healthy @@ -41,34 +41,6 @@ services: - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket - ./wallet.json:/app/wallet.json # Mount wallet.json into the container - requester: - image: randao/requester:v0.1.13 - environment: - PATH_TO_WALLET: /app/wallet.json # Path inside the container - REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - PROVIDER_ID: ${PROVIDER_ID} - DOCKER_NETWORK: backend # Passing the network name - networks: - - backend - volumes: - - ./wallet.json:/app/wallet.json # Mount wallet.json into the container - - dbeaver: - image: dbeaver/cloudbeaver:23.2.0 - environment: - CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin" - CB_SERVER_ADMIN_PASSWORD: "admin123" - depends_on: - postgres: - condition: service_healthy - networks: - - backend - ports: - - "8081:8978" # Expose the DBeaver web UI - volumes: - - dbeaver-data:/opt/cloudbeaver/workspace - networks: backend: name: backend # This will set the network name explicitly @@ -77,5 +49,3 @@ networks: volumes: pgdata: driver: local - dbeaver-data: - driver: local diff --git a/docker-compose/README.md b/docker-compose/README.md index 0f63a1b..bba1346 100644 --- a/docker-compose/README.md +++ b/docker-compose/README.md @@ -1,4 +1,4 @@ -#### Steps to Deploy: +# Steps to Deploy: 1. **Install Docker Compose:** Follow the [official Docker Compose installation guide](https://docs.docker.com/compose/install/). From a4f7964c06db5e4589a3fc7574f0bd4ba49c67e4 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 3 Feb 2025 08:22:46 -0500 Subject: [PATCH 19/80] added --- terraform/README.md | 2 +- terraform/ecs.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/terraform/README.md b/terraform/README.md index b999b0d..24f8d16 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -144,7 +144,7 @@ Example wallet JSON format: } ``` -Note: The wallet JSON should be pasted directly into the terraform.tfvars file using the heredoc syntax (< Date: Mon, 3 Feb 2025 08:31:09 -0500 Subject: [PATCH 20/80] added --- .gitignore | 1 + README.md | 4 +- docker-compose/docker-compose.yml | 10 +- execution-role-policy.json | 12 - orchestrator/package.json | 2 +- orchestrator/src/app.ts | 516 +++++++++++++++++++----------- orchestrator/src/clear_outputs.ts | 8 +- orchestrator/src/ecs_config.ts | 25 +- orchestrator/src/stake.ts | 41 +++ requester/src/app.ts | 2 +- task-definition.json | 76 ----- task-role-policy.json | 12 - terraform/README.md | 16 +- terraform/terraform.tfvars | 34 -- 14 files changed, 426 insertions(+), 333 deletions(-) delete mode 100644 execution-role-policy.json create mode 100644 orchestrator/src/stake.ts delete mode 100644 task-definition.json delete mode 100644 task-role-policy.json delete mode 100644 terraform/terraform.tfvars diff --git a/.gitignore b/.gitignore index ca600aa..c8e7a03 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ node_modules/ *.exe *.lock.* LICENSE.txt +terraform/terraform.tfvars # Ignore package locks and dependency files diff --git a/README.md b/README.md index 688ee9b..17e5b7f 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ This option may cost more and depends on the uptime of the hardware you use. It ## Graceful Shutdown Policy To avoid being slashed, it is critical to run the graceful shutdown in the event of downtime. Failing to do so may result in penalties. -To run this go to "TODO" and navigate to your node and select the "SHUT DOWN" button and sign the transaction. +To run this go to ar://randao and navigate to your node and select the "SHUT DOWN" button and sign the transaction. This will tell your provider to stop serving random. After the maintinance is done and your provider is back up again click "START UP" button. @@ -77,7 +77,7 @@ After successfully setting up your node, you will need to provide proof that the 2. Upon confirmation, Ethan will provide your provider address with the necessary funds. -3. Navigate to [Insert link here] to stake your funds and configure your provider information. +3. Navigate to ar://randao to stake your funds and configure your provider information. By completing this process, you will fully activate your node and ensure it is ready for network participation. Thank you for contributing to the network's success! \ No newline at end of file diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 32100cb..5ed36c1 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +# version: '3.8' services: # Instance 1 @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.2.18 + image: randao/orchestrator:v0.2.46 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.2.18 + image: randao/orchestrator:v0.2.46 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.2.18 + image: randao/orchestrator:v0.2.46 depends_on: postgres3: condition: service_healthy @@ -133,7 +133,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.2.0 + image: randao/requester:v0.2.1 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/execution-role-policy.json b/execution-role-policy.json deleted file mode 100644 index b833d12..0000000 --- a/execution-role-policy.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "ecs-tasks.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] -} diff --git a/orchestrator/package.json b/orchestrator/package.json index 4238a12..959d2ce 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients":"3.5.17", + "ao-process-clients":"3.6.0", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 7af74d3..9072c4b 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,17 +1,19 @@ import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; -import { getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" +import { Environment, GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" import { dbConfig } from './db_config.js'; +import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; + const RANDOM_CONFIG: RandomClientConfig = { tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", - processId: "KbaY8P4h9wdHYKHlBSLbXN_yd-9gxUDxSgBackUxTiQ", + processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", wallet: JSON.parse(process.env.WALLET_JSON!), environment: 'mainnet' } //const randclient: IRandomClient = RandomClient.autoConfiguration() -const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG) +const randclient = new RandomClient(RANDOM_CONFIG) const docker = new Docker(); @@ -19,14 +21,15 @@ const docker = new Docker(); // Constants for configuration const POLLING_INTERVAL_MS = 5000; -const MINIMUM_ENTRIES = 250; -const TARGET_ENTRIES = 500; +const MINIMUM_ENTRIES = 500; +const DRYRUNTIMEOUT = 15000; // 15 seconds +const DRYRUNRESETTIME = 300000; // 5 min //Expected increments per second=10×0.005=0.05 //180 times per hour //4,320 times per day //1,576,800 times per year -const MAX_OUTSTANDING_REQUESTS = 50; -const MAX_OUTSTANDING_FULFILLMENTS = 50; +const MAX_OUTSTANDING_VDF_CONTAINERS = 10; +const RANDOM_PER_VDF = 10; const MAX_RETRIES = 10; const RETRY_DELAY_MS = 10000; const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.4'; @@ -34,15 +37,37 @@ const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks const ongoingContainers = new Set(); // Track container IDs of running Docker containers -const ongoingFulfillments = new Set(); // Track request IDs for ongoing fulfillments const PROVIDER_ID = process.env.PROVIDER_ID || "0"; const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; +const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds let ongoingRequest = false; let spotInterruptions = 0; -let totalProvided = 0; let PreviousTotalAvailableRandom = 0; -const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds +// Global variables to track polling status +let pollingInProgress = false; +let lastPollingId: string | null = null; +// Cache for network configuration +let cachedNetworkConfig: NetworkConfig | null = null; + + +interface StepTracking { + step1?: { completed: boolean; timeTaken: number }; + step2?: { completed: boolean; timeTaken: number }; + step3?: { completed: boolean; timeTaken: number }; + step4?: { completed: boolean; timeTaken: number }; +} +let stepTracking: StepTracking = {}; // Tracks the status and time for each step + +// Function to reset step tracking data +function resetStepTracking() { + stepTracking = { + step1: { completed: false, timeTaken: 0 }, + step2: { completed: false, timeTaken: 0 }, + step3: { completed: false, timeTaken: 0 }, + step4: { completed: false, timeTaken: 0 }, + }; +} // Retry logic for connecting to PostgreSQL async function connectWithRetry(): Promise { @@ -75,8 +100,8 @@ async function setupDatabase(client: Client): Promise { ); `); - // Ensure detected_completed column exists in case the table was created before it was added - await client.query(` + // Ensure detected_completed column exists in case the table was created before it was added + await client.query(` ALTER TABLE verifiable_delay_functions ADD COLUMN IF NOT EXISTS detected_completed TIMESTAMP NULL; `); @@ -84,17 +109,11 @@ async function setupDatabase(client: Client): Promise { } -import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; - -// Cache for network configuration -let cachedNetworkConfig: NetworkConfig | null = null; - -// Modified function to trigger VDF job pod using ECS or Docker async function triggerVDFJobPod(): Promise { if (ENVIRONMENT === 'cloud') { try { console.log("Cloud environment detected. Launching ECS task."); - + // Get or refresh network configuration if (!cachedNetworkConfig) { console.log("Fetching network configuration..."); @@ -102,7 +121,7 @@ async function triggerVDFJobPod(): Promise { console.log("Network config:", cachedNetworkConfig); } - const taskArn = await launchVDFTask(ecs, cachedNetworkConfig); + const taskArn = await launchVDFTask(ecs, cachedNetworkConfig, RANDOM_PER_VDF); if (taskArn) { ongoingTasks.add(taskArn); console.log(`ECS task started successfully: ${taskArn}`); @@ -121,32 +140,44 @@ async function triggerVDFJobPod(): Promise { return null; } } else { - const containerName = `vdf_job_${Date.now()}_${Math.floor(Math.random() * 10000)}`; - console.log(`Starting Docker container with name: ${containerName}`); - const container = await docker.createContainer({ - Image: VDF_JOB_IMAGE, - Cmd: ['python', 'main.py'], - Env: [ - `DATABASE_TYPE=postgresql`, - `DATABASE_HOST=${dbConfig.host}`, - `DATABASE_PORT=${dbConfig.port.toString()}`, - `DATABASE_USER=${dbConfig.user}`, - `DATABASE_PASSWORD=${dbConfig.password}`, - `DATABASE_NAME=${dbConfig.database}`, - ], - HostConfig: { - NetworkMode: DOCKER_NETWORK, - }, - name: containerName - }); + // Check if we have reached the maximum number of containers + if (ongoingContainers.size >= MAX_OUTSTANDING_VDF_CONTAINERS) { + console.log(`Maximum outstanding VDF containers (${MAX_OUTSTANDING_VDF_CONTAINERS}) reached. Not starting new container.`); + return null; + } - await container.start(); - ongoingContainers.add(container.id); - console.log(`Docker container ${containerName} started successfully.`); - return container.id; + const containerName = `vdf_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + console.log(`Starting Docker container with name: ${containerName}`); + try { + const container = await docker.createContainer({ + Image: VDF_JOB_IMAGE, + Cmd: ['sh', '-c', `for i in $(seq 1 ${RANDOM_PER_VDF}); do python main.py; done`], + Env: [ + `DATABASE_TYPE=postgresql`, + `DATABASE_HOST=${dbConfig.host}`, + `DATABASE_PORT=${dbConfig.port.toString()}`, + `DATABASE_USER=${dbConfig.user}`, + `DATABASE_PASSWORD=${dbConfig.password}`, + `DATABASE_NAME=${dbConfig.database}`, + ], + HostConfig: { + NetworkMode: DOCKER_NETWORK, + }, + name: containerName + }); + + await container.start(); + ongoingContainers.add(container.id); + console.log(`Docker container ${containerName} started successfully.`); + return container.id; + } catch (error) { + console.error(`Error starting Docker container ${containerName}:`, error); + return null; + } } } + // Modified function to wait for ECS tasks to complete and remove them from tracking async function monitorECSTasks(): Promise { if (ongoingTasks.size === 0) return; @@ -181,34 +212,38 @@ async function monitorECSTasks(): Promise { // Function to wait for Docker containers to complete and remove them from tracking async function monitorDockerContainers(): Promise { if (ongoingContainers.size === 0) return; + if (ENVIRONMENT === 'cloud') { + await monitorECSTasks(); + } else { - for (const containerId of ongoingContainers) { - try { - const container = docker.getContainer(containerId); - const containerInfo = await container.inspect(); - - // Check if the container is already stopped (exited) - if (containerInfo.State.Status === 'exited') { - console.log(`Docker container stopped: ${containerId}`); - - // Attempt to remove the container, handling possible errors gracefully - try { - await container.remove({ force: true }); // Force removal to avoid "in progress" errors - console.log(`Docker container removed: ${containerId}`); - ongoingContainers.delete(containerId); - } catch (removeError) { - if (isDockerError(removeError) && removeError.statusCode === 409) { - // Error 409 means removal is in progress, so skip this container for now - console.log(`Removal of container ${containerId} is already in progress. Skipping.`); - } else { - // Handle other errors that might occur during container removal - console.error(`Error removing Docker container ${containerId}:`, removeError); + for (const containerId of ongoingContainers) { + try { + const container = docker.getContainer(containerId); + const containerInfo = await container.inspect(); + + // Check if the container is already stopped (exited) + if (containerInfo.State.Status === 'exited') { + console.log(`Docker container stopped: ${containerId}`); + + // Attempt to remove the container, handling possible errors gracefully + try { + await container.remove({ force: true }); // Force removal to avoid "in progress" errors + console.log(`Docker container removed: ${containerId}`); + ongoingContainers.delete(containerId); + } catch (removeError) { + if (isDockerError(removeError) && removeError.statusCode === 409) { + // Error 409 means removal is in progress, so skip this container for now + console.log(`Removal of container ${containerId} is already in progress. Skipping.`); + } else { + // Handle other errors that might occur during container removal + console.error(`Error removing Docker container ${containerId}:`, removeError); + } } } + } catch (error) { + console.error(`Error inspecting Docker container ${containerId}:`, error); + ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) } - } catch (error) { - console.error(`Error inspecting Docker container ${containerId}:`, error); - ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) } } } @@ -222,16 +257,16 @@ function isDockerError(error: unknown): error is { statusCode: number } { function hexMod64Bit(expectedOutput: string): { expectedOutput64BitBase10: string } { // Parse the hexadecimal string into a BigInt const number = BigInt(`0x${expectedOutput}`); - + // Define the 64-bit modulus (2^64 - 1) const modulus = BigInt("0x7FFFFFFFF"); - + // Keep dividing by modulus until we get a remainder less than modulus let remainder = number; while (remainder >= modulus) { remainder = remainder % modulus; } - + // Return the remainder in base 10 return { expectedOutput64BitBase10: remainder.toString(), @@ -243,55 +278,99 @@ function addHexPrefix(value: string): string { return value.startsWith('0x') ? value : `0x${value}`; } -// Function to check if there are fewer than MINIMUM_ENTRIES and fetch until TARGET_ENTRIES -async function checkAndFetchIfNeeded(client: Client): Promise { - try { - if (ongoingRequest) return; - const res = await client.query('SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL'); - const currentCount = parseInt(res.rows[0].count, 10); - console.log("Total usable db entries: " + currentCount); - if(PreviousTotalAvailableRandom !=currentCount){ - console.log(`Updating avalible random values from ${PreviousTotalAvailableRandom} to ${currentCount}`) - randclient.updateProviderAvailableValues(currentCount) - PreviousTotalAvailableRandom = currentCount; - } - - if (currentCount < MINIMUM_ENTRIES) { - const entriesNeeded = TARGET_ENTRIES - currentCount; - console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries to reach ${TARGET_ENTRIES}...`); - ongoingRequest = true; - - const batchCount = Math.min(entriesNeeded, MAX_OUTSTANDING_REQUESTS); - console.log(`Batchcount: ${batchCount}, Ongoing containers: ${ongoingContainers.size}`); - - let spawnCount = Math.min(batchCount, MAX_OUTSTANDING_REQUESTS - ongoingContainers.size); - if (spawnCount <= 0) { - console.log("Max outstanding containers reached. Skipping new container launches."); - return; - } - - for (let i = 0; i < spawnCount; i++) { - if (ENVIRONMENT === 'cloud' && ongoingTasks.size < MAX_OUTSTANDING_REQUESTS) { - triggerVDFJobPod().then(taskArn => { - if (taskArn) console.log(`ECS task triggered: ${taskArn}`); - }).catch(console.error); - } else if (ENVIRONMENT !== 'cloud' && ongoingContainers.size < MAX_OUTSTANDING_REQUESTS) { - triggerVDFJobPod().then(containerId => { - if (containerId) console.log(`Docker container triggered: ${containerId}`); - }).catch(console.error); +function updateAvailableValuesAsync(currentCount: number) { + return (async () => { + try { + await randclient.updateProviderAvailableValues(currentCount); + console.log(`Updated provider values to ${currentCount}`); + } catch (error) { + console.error("Failed to update provider values:", error); + } + })(); +} + +async function getMoreRandom(currentCount: number) { + const entriesNeeded = MINIMUM_ENTRIES - currentCount; + console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); + + ongoingRequest = true; + + // Calculate how many containers to spawn + const possibleBatchCount = Math.ceil(entriesNeeded / RANDOM_PER_VDF); + const availableSpawns = Math.min( + possibleBatchCount, + MAX_OUTSTANDING_VDF_CONTAINERS - ongoingContainers.size + ); + + if (availableSpawns <= 0) { + console.log("Max outstanding containers reached. Skipping new container launches."); + ongoingRequest = false; + return; + } + + console.log(`Spawning up to ${availableSpawns} containers to generate random values.`); + + for (let i = 0; i < availableSpawns; i++) { + try { + const jobId = await triggerVDFJobPod(); + if (jobId) { + console.log(`Job triggered: ${jobId}`); + ongoingContainers.add(jobId); } + } catch (error) { + console.error('Error triggering job pod:', error); } + } +} + + +// Function to check and fetch database entries as needed +async function checkAndFetchIfNeeded(client: Client) { + try { + //Check if provider has been given a special signal + const on_chain_avalible_random = await getProviderAvailableRandomValues(PROVIDER_ID); - ongoingRequest = false; // Immediately allow other operations + // Query current count of usable DB entries + const res = await client.query( + 'SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL' + ); + const currentCount = parseInt(res.rows[0].count, 10); + console.log("Total usable DB entries: " + currentCount); + + switch (on_chain_avalible_random.availibleRandomValues) { + case -1: + console.log("Provider is shutting down"); + //TODO prepare for shutdown + break; + case -2: + console.log("Value is -2"); + break; + case -3: + console.log("Value is -3"); + break; + default: + console.log("Value is not -1, -2, or -3"); + if (PreviousTotalAvailableRandom !== currentCount) { + console.log(`Updating available random values from ${PreviousTotalAvailableRandom} to ${currentCount}`); + updateAvailableValuesAsync(currentCount); + PreviousTotalAvailableRandom = currentCount; + } } + if (ongoingRequest) return; // Prevent redundant operations + + // Check if more entries are needed + if (currentCount >= MINIMUM_ENTRIES) return; + getMoreRandom(currentCount) + } catch (error) { console.error('Error during check and fetch:', error); - ongoingRequest = false; + } finally { + ongoingRequest = false; // Allow future operations } } // Function to post VDF challenge (fetches dbId dynamically) -async function fulfillRandomChallenge(client: Client, requestId: string): Promise { +async function fulfillRandomChallenge(client: Client, requestId: string, parentLogId: string): Promise { try { // Fetch the necessary details from the database using requestId const res = await client.query( @@ -308,24 +387,24 @@ async function fulfillRandomChallenge(client: Client, requestId: string): Promis const { id: dbId, modulus, input } = res.rows[0]; - console.log(`Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); + console.log(`${parentLogId} Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); // Add hex prefix to modulus and input const hexModulus = addHexPrefix(modulus); const hexInput = addHexPrefix(input); - console.log(`Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); + console.log(`${parentLogId} Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); await randclient.postVDFChallenge(requestId, hexModulus, hexInput); - console.log(`Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); + console.log(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); } catch (error) { - console.error(`Error posting VDF challenge for Request ID: ${requestId}:`, error); + console.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); } } // Function to post VDF output and proof -async function fulfillRandomOutput(client: Client, requestId: string): Promise { +async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { try { // Fetch the output and proof from the database using the requestId const res = await client.query('SELECT id, output, proof FROM verifiable_delay_functions WHERE request_id = $1', [requestId]); @@ -335,11 +414,11 @@ async function fulfillRandomOutput(client: Client, requestId: string): Promise { + + let openRequests: GetOpenRandomRequestsResponse; -async function polling(client: Client): Promise { + // Create a function to fetch open requests with a timeout + const fetchOpenRequests = async (): Promise => { + try { + const response = await randclient.getOpenRandomRequests(PROVIDER_ID); + return response; + } catch (error) { + console.error(`${parentLogId} Error fetching requests: ${error}`); + return { /* Return a default or empty response here */ } as GetOpenRandomRequestsResponse; + } + }; + + try { + openRequests = await Promise.race([ + fetchOpenRequests().catch(err => { + throw new Error(`${parentLogId} Fetch Error: ${err}`); + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), DRYRUNTIMEOUT) + ) + ]); + } catch (error) { + console.log(`${parentLogId} Step 1: ${error}`); + randclient.setDryRunAsMessage(true); + console.log("Switching dryrun off"); + openRequests = await fetchOpenRequests(); // Retry request + } + + console.log(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(openRequests)}`); + console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeChallengeRequests.request_ids.length}`); + console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeOutputRequests.request_ids.length}`); + return openRequests; +} + +//Todo timeout the second anddefault to 0 +async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise { + let avalibleRandom: GetProviderAvailableValuesResponse; + // Create a function to fetch open requests with a timeout + const fetchAvalibleRandom = async (): Promise => { + try { + const response = await randclient.getProviderAvailableValues(PROVIDER_ID); + return response; + } catch (error) { + console.error(`Error fetching avalible random: ${error}`); + return { /* Return a default or empty response here */ } as GetProviderAvailableValuesResponse; + } + }; + + try { + avalibleRandom = await Promise.race([ + fetchAvalibleRandom().catch(err => { + throw new Error(`Fetch Error: ${err}`); + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), DRYRUNTIMEOUT) + ) + ]); + } catch (error) { + // console.log(`${parentLogId} Step 1: ${error}`); + randclient.setDryRunAsMessage(true); + console.log("Switching dryrun off"); + avalibleRandom = await fetchAvalibleRandom(); // Retry request + } + return avalibleRandom; +} + + + +async function polling(client: any) { if (pollingInProgress) { - console.log(`[SKIPPED] Polling is already in progress. Skipping this run.`); + const completedSteps = Object.entries(stepTracking) + .filter(([_, data]) => data?.completed) + .map(([step, data]) => `${step} (Time: ${data?.timeTaken}ms)`); + + console.log(`\n[SKIPPED] Polling already in progress for ${lastPollingId}. Skipping this run.`); + console.log(`Completed steps so far: ${completedSteps.length > 0 ? completedSteps.join(", ") : "None"}`); + console.log("Current step tracking status:", stepTracking); // Debugging info to inspect tracking object return; // Prevent concurrent execution } + resetStepTracking(); // Reset step tracking for fresh polling pollingInProgress = true; // Mark polling as in progress const logId = getLogId(); + lastPollingId = logId; console.log(`${logId} Starting Polling...`); try { const startTime = Date.now(); // Start time of polling - console.log(`${logId} Step 1: Fetching open requests from the Randomness Client.`); - const step1Start = Date.now(); - const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); - const step1End = Date.now(); - console.log(`${logId} Step 1: Open Requests: ${JSON.stringify(openRequests)}`); - console.log(openRequests); - console.log(`${logId} Step 1: Open requests fetched. Time taken: ${(step1End - step1Start)}ms`); + // Step 1: Fetch open requests + const s1 = Date.now(); + console.log(`${logId} Step 1 started.`); + const openRequests = await getProviderRequests(PROVIDER_ID, logId); + stepTracking.step1 = { completed: true, timeTaken: Date.now() - s1 }; + console.log(`${logId} Step 1: Open requests fetched. Time taken: ${stepTracking.step1.timeTaken}ms`); - // Run Step 2, 3, and 4 concurrently after Step 1 - const step2Start = Date.now(); + // Run Step 2, 3, and 4 concurrently await Promise.all([ (async () => { const s2 = Date.now(); + console.log(`${logId} Step 2 started.`); await processChallengeRequests(client, openRequests.activeChallengeRequests, logId); - console.log(`${logId} Step 2 completed. Time taken: ${Date.now() - s2}ms`); + stepTracking.step2 = { completed: true, timeTaken: Date.now() - s2 }; + console.log(`${logId} Step 2 completed. Time taken: ${stepTracking.step2.timeTaken}ms`); })(), (async () => { const s3 = Date.now(); + console.log(`${logId} Step 3 started.`); await processOutputRequests(client, openRequests.activeOutputRequests, logId); - console.log(`${logId} Step 3 completed. Time taken: ${Date.now() - s3}ms`); + stepTracking.step3 = { completed: true, timeTaken: Date.now() - s3 }; + console.log(`${logId} Step 3 completed. Time taken: ${stepTracking.step3.timeTaken}ms`); })(), (async () => { const s4 = Date.now(); + console.log(`${logId} Step 4 started.`); await cleanupFulfilledEntries(client, openRequests, logId); - console.log(`${logId} Step 4 completed. Time taken: ${Date.now() - s4}ms`); + stepTracking.step4 = { completed: true, timeTaken: Date.now() - s4 }; + console.log(`${logId} Step 4 completed. Time taken: ${stepTracking.step4.timeTaken}ms`); })(), ]); - const step2End = Date.now(); - const totalTime = step2End - startTime; + const totalTime = Date.now() - startTime; console.log(`${logId} Polling cycle completed successfully. Total time taken: ${totalTime}ms`); } catch (error) { @@ -425,39 +582,37 @@ async function processChallengeRequests( activeChallengeRequests: { request_ids: string[] } | undefined, parentLogId: string ): Promise { - const logId = getLogId(); - console.log(`${logId} Step 2: Processing challenge requests.`); + console.log(`${parentLogId} Step 2: Processing challenge requests.`); if (!activeChallengeRequests || activeChallengeRequests.request_ids.length === 0) { - console.log(`${logId} No Challenge Requests to process.`); + console.log(`${parentLogId} No Challenge Requests to process.`); return; } - // Limit to MAX_OUTSTANDING_FULFILLMENTS requests - const requestIds = activeChallengeRequests.request_ids.slice(0, MAX_OUTSTANDING_FULFILLMENTS); - console.log(`${logId} Processing up to ${requestIds.length} requests.`); + const requestIds = activeChallengeRequests.request_ids; + console.log(`${parentLogId} Processing up to ${requestIds.length} requests.`); try { await client.query('BEGIN'); // Start transaction - console.log(`${logId} Fetching existing request mappings.`); + console.log(`${parentLogId} Fetching existing request mappings.`); // Fetch already assigned request_id -> dbId mappings const existingMappingsRes = await client.query( `SELECT request_id FROM verifiable_delay_functions WHERE request_id = ANY($1) - FOR UPDATE SKIP LOCKED`, + FOR UPDATE SKIP LOCKED`, [requestIds] ); const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); - console.log(`${logId} Found ${existingRequestIds.size} already mapped requests.`); + console.log(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); // Find only the unmapped requests (requestIds not in existingRequestIds) const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); - console.log(`${logId} Unmapped requests: ${unmappedRequestIds.length}`); + console.log(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); // Fetch available DB entries for unmapped requests - console.log(`${logId} Fetching available DB entries.`); + console.log(`${parentLogId} Fetching available DB entries.`); const dbRes = await client.query( `SELECT id FROM verifiable_delay_functions WHERE request_id IS NULL @@ -468,16 +623,16 @@ async function processChallengeRequests( ); const availableDbEntries = dbRes.rows.map(row => row.id); - console.log(`${logId} Found ${availableDbEntries.length} available DB entries.`); + console.log(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); // Reduce request list if we don’t have enough DB entries if (availableDbEntries.length < unmappedRequestIds.length) { - console.log(`${logId} Limiting requests to ${availableDbEntries.length} due to DB availability.`); + console.log(`${parentLogId} Limiting requests to ${availableDbEntries.length} due to DB availability.`); unmappedRequestIds.length = availableDbEntries.length; } if (availableDbEntries.length === 0 && existingRequestIds.size === 0) { - console.log(`${logId} No available DB entries to process and no existing mappings.`); + console.log(`${parentLogId} No available DB entries to process and no existing mappings.`); await client.query('COMMIT'); // Commit to release locks return; } @@ -490,52 +645,51 @@ async function processChallengeRequests( WHERE id = $2`, [unmappedRequestIds[i], availableDbEntries[i]] ); - console.log(`${logId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); + console.log(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); } await client.query('COMMIT'); // Commit all updates at once // Call fulfillRandomChallenge for all request IDs (existing + newly mapped) - for (const requestId of requestIds) { - fulfillRandomChallenge(client, requestId) - .catch(error => console.error(`${logId} Error fulfilling challenge for Request ID ${requestId}:`, error)); - } + // Create an array of promises and use Promise.all to await them all in parallel + const promises = requestIds.map(requestId => + fulfillRandomChallenge(client, requestId, parentLogId) + .catch(error => console.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) + ); + + await Promise.all(promises); // Wait for all promises to resolve + console.log(`${parentLogId} All challenges fulfilled`); - console.log(`${logId} Step 2 completed.`); + console.log(`${parentLogId} Step 2 completed.`); } catch (error) { - console.error(`${logId} Error in processChallengeRequests:`, error); + console.error(`${parentLogId} Error in processChallengeRequests:`, error); await client.query('ROLLBACK'); // Rollback on failure } } - - - - // Step 3: Process Output Requests (unchanged but with logging) async function processOutputRequests( client: Client, activeOutputRequests: { request_ids: string[] } | undefined, parentLogId: string ): Promise { - const logId = getLogId(); - console.log(`${logId} Step 3: Processing output requests.`); + console.log(`${parentLogId} Step 3: Processing output requests.`); if (!activeOutputRequests || activeOutputRequests.request_ids.length === 0) { - console.log(`${logId} No Output Requests to process.`); + console.log(`${parentLogId} No Output Requests to process.`); return; } const outputPromises = activeOutputRequests.request_ids.map(async (requestId) => { - console.log(`${logId} Processing output request ID: ${requestId}`); + console.log(`${parentLogId} Processing output request ID: ${requestId}`); // Run fulfillRandomOutput asynchronously (do not await) - fulfillRandomOutput(client, requestId) - .catch(error => console.error(`${logId} Error fulfilling output:`, error)); + fulfillRandomOutput(client, requestId, parentLogId) + .catch(error => console.error(`${parentLogId} Error fulfilling output:`, error)); }); await Promise.all(outputPromises); - console.log(`${logId} Step 3 completed.`); + console.log(`${parentLogId} Step 3 completed.`); } // Step 4: Remove fulfilled entries no longer in use (unchanged but with logging) @@ -544,8 +698,7 @@ async function cleanupFulfilledEntries( openRequests: any, parentLogId: string ): Promise { - const logId = getLogId(); - console.log(`${logId} Step 4: Checking for fulfilled entries no longer in use.`); + console.log(`${parentLogId} Step 4: Checking for fulfilled entries no longer in use.`); const now = new Date(); const cutoffTime = new Date(now.getTime() - COMPLETION_RETENTION_PERIOD_MS); @@ -588,7 +741,7 @@ async function cleanupFulfilledEntries( SET detected_completed = NOW() WHERE id = ANY($1) `, [markAsCompleted]); - console.log(`${logId} Marked ${markAsCompleted.length} entries as completed.`); + console.log(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); } // Delete old completed entries @@ -597,16 +750,16 @@ async function cleanupFulfilledEntries( DELETE FROM verifiable_delay_functions WHERE id = ANY($1) `, [markForDeletion]); - console.log(`${logId} Deleted ${markForDeletion.length} old completed entries.`); + console.log(`${parentLogId} Deleted ${markForDeletion.length} old completed entries.`); } await client.query('COMMIT'); } catch (error) { - console.error(`${logId} Error in cleanupFulfilledEntries:`, error); + console.error(`${parentLogId} Error in cleanupFulfilledEntries:`, error); await client.query('ROLLBACK'); } - console.log(`${logId} Step 4 completed.`); + console.log(`${parentLogId} Step 4 completed.`); } @@ -620,22 +773,27 @@ async function run(): Promise { const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); console.log(`Periodic log - Current database size: ${res.rows[0].count}`); // Check and fetch entries for the database if needed - console.log("Step 1: Checking and fetching database entries if below threshold."); + console.log("Step 0: Checking and fetching database entries if below threshold."); checkAndFetchIfNeeded(client).catch((error) => { console.error("Error in checkAndFetchIfNeeded:", error); }); - + }, 10000); setInterval(async () => { await monitorDockerContainers(); - await monitorECSTasks(); }, 30000); // Cleanup every 30 seconds setInterval(async () => { await polling(client); }, POLLING_INTERVAL_MS); + setInterval(async () => { + randclient.setDryRunAsMessage(false); + console.log("Switching dryrun on") + }, DRYRUNRESETTIME); + + process.on("SIGTERM", async () => { console.log("SIGTERM received. Closing database connection."); await client.end(); diff --git a/orchestrator/src/clear_outputs.ts b/orchestrator/src/clear_outputs.ts index d7ee4c3..a554e4d 100644 --- a/orchestrator/src/clear_outputs.ts +++ b/orchestrator/src/clear_outputs.ts @@ -4,11 +4,11 @@ import { dbConfig } from './db_config'; // Random Client Configuration const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: "7enZBOhWsyU3A5oCt8HtMNNPHSxXYJVTlOGOetR9IDw", - processId: "KbaY8P4h9wdHYKHlBSLbXN_yd-9gxUDxSgBackUxTiQ", + tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", + processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", wallet: JSON.parse(process.env.WALLET_JSON!), - environment: 'mainnet' as const -}; + environment: 'mainnet' +} const randclient = new RandomClient(RANDOM_CONFIG); const PROVIDER_ID = process.env.PROVIDER_ID || "0"; diff --git a/orchestrator/src/ecs_config.ts b/orchestrator/src/ecs_config.ts index 18f30db..e97184a 100644 --- a/orchestrator/src/ecs_config.ts +++ b/orchestrator/src/ecs_config.ts @@ -34,26 +34,39 @@ export async function getNetworkConfig(ecs: AWS.ECS): Promise { }; } -export async function launchVDFTask(ecs: AWS.ECS, networkConfig: NetworkConfig): Promise { +export async function launchVDFTask( + ecs: AWS.ECS, + networkConfig: NetworkConfig, + random_per_vdf: number, +): Promise { const result = await ecs.runTask({ cluster: process.env.ECS_CLUSTER_NAME, taskDefinition: 'vdf-job', capacityProviderStrategy: [ { capacityProvider: 'FARGATE_SPOT', - weight: 1 - } + weight: 1, + }, ], networkConfiguration: { awsvpcConfiguration: { subnets: networkConfig.subnets, securityGroups: networkConfig.securityGroups, - assignPublicIp: 'ENABLED' - } + assignPublicIp: 'ENABLED', + }, + }, + count: 1, + overrides: { + containerOverrides: [ + { + name: 'vdf_job_container', // Must match the container name in the task definition + command: ['sh', '-c', `for i in $(seq 1 ${random_per_vdf}); do python main.py; done`], + }, + ], }, - count: 1 }).promise(); const taskArn = result.tasks?.[0]?.taskArn; return taskArn || null; } + diff --git a/orchestrator/src/stake.ts b/orchestrator/src/stake.ts new file mode 100644 index 0000000..9ed352f --- /dev/null +++ b/orchestrator/src/stake.ts @@ -0,0 +1,41 @@ +import { getRandomClientAutoConfiguration, RandomClient, RandomClientConfig, StakingClient, } from "ao-process-clients"; +import { ProviderDetails } from "ao-process-clients/dist/src/clients/staking/abstract/types"; + +// Random Client Configuration +const RANDOM_CONFIG: RandomClientConfig = { + tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", + processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", + wallet: JSON.parse(process.env.WALLET_JSON!), + environment: 'mainnet' +} + +const randclient = new StakingClient(RANDOM_CONFIG); + +// Function to clear all output requests +async function stake() { + try { + let providerDetails: ProviderDetails = { /** Provider name */ + name: "test", + /** Commission percentage (1-100) */ + commission: 50, + /** Provider description */ + description: "this is a test description", + /** Optional Twitter handle */ + twitter: "test_twitter", + /** Optional Discord handle */ + discord: "test_discord", + /** Optional Telegram handle */ + telegram: "test_tg"}; + console.log(await randclient.stake("100000000000000000000",providerDetails)) + } catch (error) { + console.error("An error occurred while staking:", error); + } finally { + + console.log("Done."); + } +} + +// Run the function when the script is executed +(async () => { + await stake(); +})(); diff --git a/requester/src/app.ts b/requester/src/app.ts index 99b2bd6..142f2d2 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -17,7 +17,7 @@ const CHANCE_TO_CALL_RANDOM = 1; const RANDOM_CONFIG: RandomClientConfig = { tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", - processId: "KbaY8P4h9wdHYKHlBSLbXN_yd-9gxUDxSgBackUxTiQ", + processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", wallet: JSON.parse(process.env.REQUEST_WALLET_JSON!), environment: "mainnet", }; diff --git a/task-definition.json b/task-definition.json deleted file mode 100644 index cf7e617..0000000 --- a/task-definition.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "family": "orchestrator-service", - "networkMode": "awsvpc", - "containerDefinitions": [ - { - "name": "postgres", - "image": "postgres:13", - "essential": true, - "environment": [ - { "name": "POSTGRES_USER", "value": "myuser" }, - { "name": "POSTGRES_PASSWORD", "value": "mypassword" }, - { "name": "POSTGRES_DB", "value": "mydatabase" } - ], - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "ecs-orchestrator-service", - "awslogs-region": "us-east-1", - "awslogs-stream-prefix": "postgres" - } - }, - "healthCheck": { - "command": [ - "CMD-SHELL", - "pg_isready -U myuser -d mydatabase" - ], - "interval": 10, - "timeout": 5, - "retries": 5 - }, - "mountPoints": [ - { - "sourceVolume": "pgdata", - "containerPath": "/var/lib/postgresql/data" - } - ] - }, - { - "name": "orchestrator", - "image": "satoshispalace/orchestrator:latest", - "essential": true, - "environment": [ - { "name": "DB_HOST", "value": "postgres" }, - { "name": "DB_PORT", "value": "5432" }, - { "name": "DB_USER", "value": "myuser" }, - { "name": "DB_PASSWORD", "value": "mypassword" }, - { "name": "DB_NAME", "value": "mydatabase" } - ], - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "ecs-orchestrator-service", - "awslogs-region": "us-east-1", - "awslogs-stream-prefix": "orchestrator" - } - }, - "mountPoints": [] - } - ], - "volumes": [ - { - "name": "pgdata", - "efsVolumeConfiguration": { - "fileSystemId": "fs-01a952c26605adac6", - "rootDirectory": "/" - } - } - ], - "requiresCompatibilities": [ - "FARGATE" - ], - "cpu": "512", - "memory": "1024", - "executionRoleArn": "arn:aws:iam::615299754404:role/orchestrator-service-execution-role", - "taskRoleArn": "arn:aws:iam::615299754404:role/orchestrator-service-task-role" -} diff --git a/task-role-policy.json b/task-role-policy.json deleted file mode 100644 index b833d12..0000000 --- a/task-role-policy.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "ecs-tasks.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] -} diff --git a/terraform/README.md b/terraform/README.md index 24f8d16..3b8fa5c 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -105,7 +105,21 @@ export AWS_REGION="your-region" # e.g., us-east-1 ``` -3. **Initialize and Apply Terraform Configuration:** + +4. **Set up ENV variables:** + Navigate to the Terraform directory of the project: + ```bash + cp terraform.tfvars.example terraform.tfvars + ``` + Fill in all of the variables with your info. + The only feilds that NEED to be filled in are + 1. provider_id + 2. local_wallet_json + + The Database configuration is highly suggested and the secrets can be left alone as its just the name of the secrets + + +5. **Initialize and Apply Terraform Configuration:** Navigate to the Terraform directory of the project and run: ```bash terraform init diff --git a/terraform/terraform.tfvars b/terraform/terraform.tfvars deleted file mode 100644 index 9c228d6..0000000 --- a/terraform/terraform.tfvars +++ /dev/null @@ -1,34 +0,0 @@ -# AWS Region -aws_region = "us-east-1" - -# Provider Configuration -provider_id = "1zlA7nKecUGevGNAEbjim_SlbioOI6daNNn2luDEHb0" # Set this to your unique provider identifier - -# Database Configuration (for local development/testing) -local_db_user = "myuser" # Change this -local_db_password = "mypassword" # Change this -db_name = "orchestrator_db" - -# Wallet Configuration -# Option 1: Paste your wallet JSON directly (for testing only) -local_wallet_json = < Date: Mon, 3 Feb 2025 08:36:32 -0500 Subject: [PATCH 21/80] added --- .env.example | 8 -- .env.template | 23 ---- .gitignore | 2 +- docker-compose/2docker-compose.yml | 51 --------- docker-compose/README.md | 18 +++ docker-compose/dev-docker-compose.yml | 154 ++++++++++++++++++++++++++ docker-compose/docker-compose.yml | 145 ++++-------------------- terraform/README.md | 67 +++-------- 8 files changed, 209 insertions(+), 259 deletions(-) delete mode 100644 .env.example delete mode 100644 .env.template delete mode 100644 docker-compose/2docker-compose.yml create mode 100644 docker-compose/dev-docker-compose.yml diff --git a/.env.example b/.env.example deleted file mode 100644 index 9d969ba..0000000 --- a/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -DB_USER=myuser -DB_PASSWORD=mypassword -DB_NAME=mydatabase -DOCKER_NETWORK=backend -PATH_TO_WALLET="wallet.json" -PROVIDER_ID = "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE" -WALLET_JSON = '{}' -REQUEST_WALLET_JSON = '{}' \ No newline at end of file diff --git a/.env.template b/.env.template deleted file mode 100644 index b741487..0000000 --- a/.env.template +++ /dev/null @@ -1,23 +0,0 @@ -# Instance 1 -DB_USER_1=myuser1 -DB_PASSWORD_1=mypassword1 -DB_NAME_1=mydatabase1 -WALLET_JSON_1=your_wallet_json_1 -REQUEST_WALLET_JSON_1=your_request_wallet_json_1 -PROVIDER_ID_1=your_provider_id_1 - -# Instance 2 -DB_USER_2=myuser2 -DB_PASSWORD_2=mypassword2 -DB_NAME_2=mydatabase2 -WALLET_JSON_2=your_wallet_json_2 -REQUEST_WALLET_JSON_2=your_request_wallet_json_2 -PROVIDER_ID_2=your_provider_id_2 - -# Instance 3 -DB_USER_3=myuser3 -DB_PASSWORD_3=mypassword3 -DB_NAME_3=mydatabase3 -WALLET_JSON_3=your_wallet_json_3 -REQUEST_WALLET_JSON_3=your_request_wallet_json_3 -PROVIDER_ID_3=your_provider_id_3 diff --git a/.gitignore b/.gitignore index c8e7a03..8e1d01f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ node_modules/ *.exe *.lock.* LICENSE.txt -terraform/terraform.tfvars +terraform.tfvars # Ignore package locks and dependency files diff --git a/docker-compose/2docker-compose.yml b/docker-compose/2docker-compose.yml deleted file mode 100644 index d48a879..0000000 --- a/docker-compose/2docker-compose.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: '3.8' - -services: - postgres: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER:-myuser} - POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword} - POSTGRES_DB: ${DB_NAME:-mydatabase} - ports: - - "5432:5432" - networks: - - backend - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-mydatabase}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator: - image: randao/orchestrator:v0.2.18 - depends_on: - postgres: - condition: service_healthy - environment: - DB_HOST: postgres - DB_PORT: 5432 - DB_USER: ${DB_USER:-myuser} - DB_PASSWORD: ${DB_PASSWORD:-mypassword} - DB_NAME: ${DB_NAME:-mydatabase} - ENVIRONMENT: local - PATH_TO_WALLET: /app/wallet.json # Path inside the container - WALLET_JSON: ${WALLET_JSON} - PROVIDER_ID: ${PROVIDER_ID} - DOCKER_NETWORK: backend # Passing the network name - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket - - ./wallet.json:/app/wallet.json # Mount wallet.json into the container - -networks: - backend: - name: backend # This will set the network name explicitly - driver: bridge - -volumes: - pgdata: - driver: local diff --git a/docker-compose/README.md b/docker-compose/README.md index bba1346..28bf61c 100644 --- a/docker-compose/README.md +++ b/docker-compose/README.md @@ -2,7 +2,25 @@ 1. **Install Docker Compose:** Follow the [official Docker Compose installation guide](https://docs.docker.com/compose/install/). + 2. **Deploy Node:** + Navigate to the Docker Compose directory and run: + ```bash + cp .env.example .env + ``` +Then fill in all of the variables: +Required variables: +- `provider_id`: Your unique provider identifier +- `local_db_user`: Database username +- `local_db_password`: Database password +- `local_wallet_json`: Wallet JSON (either direct or via file) + +Optional variables (with defaults): +- `aws_region`: AWS region (default: us-east-1) +- `db_name`: Database name (default: orchestrator_db) +- `secrets_prefix`: Prefix for secrets (default: /orchestrator) + +3. **Deploy Node:** Navigate to the Docker Compose directory and run: ```bash docker-compose up -d diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml new file mode 100644 index 0000000..5ed36c1 --- /dev/null +++ b/docker-compose/dev-docker-compose.yml @@ -0,0 +1,154 @@ +# version: '3.8' + +services: + # Instance 1 + postgres1: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_1:-myuser1} + POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword1} + POSTGRES_DB: ${DB_NAME_1:-mydatabase1} + ports: + - "5432:5432" + networks: + - backend + volumes: + - pgdata1:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser1} -d ${DB_NAME_1:-mydatabase1}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator1: + image: randao/orchestrator:v0.2.46 + depends_on: + postgres1: + condition: service_healthy + environment: + DB_HOST: postgres1 + DB_PORT: 5432 + DB_USER: ${DB_USER_1:-myuser1} + DB_PASSWORD: ${DB_PASSWORD_1:-mypassword1} + DB_NAME: ${DB_NAME_1:-mydatabase1} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_1} + PROVIDER_ID: ${PROVIDER_ID_1} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 2 + postgres2: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_2:-myuser2} + POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword2} + POSTGRES_DB: ${DB_NAME_2:-mydatabase2} + ports: + - "5433:5432" + networks: + - backend + volumes: + - pgdata2:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser2} -d ${DB_NAME_2:-mydatabase2}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator2: + image: randao/orchestrator:v0.2.46 + depends_on: + postgres2: + condition: service_healthy + environment: + DB_HOST: postgres2 + DB_PORT: 5432 + DB_USER: ${DB_USER_2:-myuser2} + DB_PASSWORD: ${DB_PASSWORD_2:-mypassword2} + DB_NAME: ${DB_NAME_2:-mydatabase2} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_2} + PROVIDER_ID: ${PROVIDER_ID_2} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 3 + postgres3: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_3:-myuser3} + POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword3} + POSTGRES_DB: ${DB_NAME_3:-mydatabase3} + ports: + - "5434:5432" + networks: + - backend + volumes: + - pgdata3:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser3} -d ${DB_NAME_3:-mydatabase3}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator3: + image: randao/orchestrator:v0.2.46 + depends_on: + postgres3: + condition: service_healthy + environment: + DB_HOST: postgres3 + DB_PORT: 5432 + DB_USER: ${DB_USER_3:-myuser3} + DB_PASSWORD: ${DB_PASSWORD_3:-mypassword3} + DB_NAME: ${DB_NAME_3:-mydatabase3} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_3} + PROVIDER_ID: ${PROVIDER_ID_3} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Single DBeaver Instance + dbeaver: + image: dbeaver/cloudbeaver:23.2.0 + environment: + CB_SERVER_SERVER_PORT: 8080 + CB_SERVER_ADMIN_NAME: "admin" + CB_SERVER_ADMIN_PASSWORD: "admin123" + networks: + - backend + ports: + - "8080:8978" + volumes: + - dbeaver-data:/opt/cloudbeaver/workspace + + requester: + image: randao/requester:v0.2.1 + environment: + REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} + + +networks: + backend: + name: backend + driver: bridge + +volumes: + pgdata1: + driver: local + pgdata2: + driver: local + pgdata3: + driver: local + dbeaver-data: + driver: local diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 5ed36c1..b002fb5 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,154 +1,51 @@ -# version: '3.8' +version: '3.8' services: - # Instance 1 - postgres1: + postgres: image: postgres:13 environment: - POSTGRES_USER: ${DB_USER_1:-myuser1} - POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword1} - POSTGRES_DB: ${DB_NAME_1:-mydatabase1} + POSTGRES_USER: ${DB_USER:-myuser} + POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword} + POSTGRES_DB: ${DB_NAME:-mydatabase} ports: - "5432:5432" networks: - backend volumes: - - pgdata1:/var/lib/postgresql/data + - pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser1} -d ${DB_NAME_1:-mydatabase1}"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-mydatabase}"] interval: 10s timeout: 5s retries: 5 - orchestrator1: + orchestrator: image: randao/orchestrator:v0.2.46 depends_on: - postgres1: + postgres: condition: service_healthy environment: - DB_HOST: postgres1 + DB_HOST: postgres DB_PORT: 5432 - DB_USER: ${DB_USER_1:-myuser1} - DB_PASSWORD: ${DB_PASSWORD_1:-mypassword1} - DB_NAME: ${DB_NAME_1:-mydatabase1} + DB_USER: ${DB_USER:-myuser} + DB_PASSWORD: ${DB_PASSWORD:-mypassword} + DB_NAME: ${DB_NAME:-mydatabase} ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_1} - PROVIDER_ID: ${PROVIDER_ID_1} - DOCKER_NETWORK: backend + PATH_TO_WALLET: /app/wallet.json # Path inside the container + WALLET_JSON: ${WALLET_JSON} + PROVIDER_ID: ${PROVIDER_ID} + DOCKER_NETWORK: backend # Passing the network name networks: - backend volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 2 - postgres2: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_2:-myuser2} - POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword2} - POSTGRES_DB: ${DB_NAME_2:-mydatabase2} - ports: - - "5433:5432" - networks: - - backend - volumes: - - pgdata2:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser2} -d ${DB_NAME_2:-mydatabase2}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator2: - image: randao/orchestrator:v0.2.46 - depends_on: - postgres2: - condition: service_healthy - environment: - DB_HOST: postgres2 - DB_PORT: 5432 - DB_USER: ${DB_USER_2:-myuser2} - DB_PASSWORD: ${DB_PASSWORD_2:-mypassword2} - DB_NAME: ${DB_NAME_2:-mydatabase2} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_2} - PROVIDER_ID: ${PROVIDER_ID_2} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 3 - postgres3: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_3:-myuser3} - POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword3} - POSTGRES_DB: ${DB_NAME_3:-mydatabase3} - ports: - - "5434:5432" - networks: - - backend - volumes: - - pgdata3:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser3} -d ${DB_NAME_3:-mydatabase3}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator3: - image: randao/orchestrator:v0.2.46 - depends_on: - postgres3: - condition: service_healthy - environment: - DB_HOST: postgres3 - DB_PORT: 5432 - DB_USER: ${DB_USER_3:-myuser3} - DB_PASSWORD: ${DB_PASSWORD_3:-mypassword3} - DB_NAME: ${DB_NAME_3:-mydatabase3} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_3} - PROVIDER_ID: ${PROVIDER_ID_3} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Single DBeaver Instance - dbeaver: - image: dbeaver/cloudbeaver:23.2.0 - environment: - CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin" - CB_SERVER_ADMIN_PASSWORD: "admin123" - networks: - - backend - ports: - - "8080:8978" - volumes: - - dbeaver-data:/opt/cloudbeaver/workspace - - requester: - image: randao/requester:v0.2.1 - environment: - REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - + - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket + - ./wallet.json:/app/wallet.json # Mount wallet.json into the container networks: backend: - name: backend + name: backend # This will set the network name explicitly driver: bridge volumes: - pgdata1: - driver: local - pgdata2: - driver: local - pgdata3: - driver: local - dbeaver-data: + pgdata: driver: local diff --git a/terraform/README.md b/terraform/README.md index 3b8fa5c..d5d73b8 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -112,9 +112,18 @@ cp terraform.tfvars.example terraform.tfvars ``` Fill in all of the variables with your info. - The only feilds that NEED to be filled in are - 1. provider_id - 2. local_wallet_json +## Variables Reference + +Required variables: +- `provider_id`: Your unique provider identifier +- `local_db_user`: Database username +- `local_db_password`: Database password +- `local_wallet_json`: Wallet JSON (either direct or via file) + +Optional variables (with defaults): +- `aws_region`: AWS region (default: us-east-1) +- `db_name`: Database name (default: orchestrator_db) +- `secrets_prefix`: Prefix for secrets (default: /orchestrator) The Database configuration is highly suggested and the secrets can be left alone as its just the name of the secrets @@ -127,7 +136,9 @@ ``` Type `yes` when prompted to confirm. -This setup ensures your node is deployed with the highest uptime and optimal performance. +This setup ensures your node is deployed with the highest uptime and optimal performance. + +Please open up the AWS console and show the logs of this to Ethan top receive the Tokens to stake # Terraform Configuration for Randomness Provider @@ -138,42 +149,6 @@ This Terraform configuration sets up: - VDF Fargate spot job configuration - AWS Secrets Manager for sensitive data -## Setup Instructions - -1. Copy the example variables file: -```bash -cp terraform.tfvars.example terraform.tfvars -``` - -2. Edit `terraform.tfvars` with your configuration: - - Set your database credentials - - Set your provider ID - - Copy your wallet JSON and paste it into the `local_wallet_json` variable - -Example wallet JSON format: -```json -{ - "address": "your-wallet-address", - "privateKey": "your-private-key" -} -``` - -Note: The wallet JSON should be pasted directly into the terraform.tfvars file - -3. Initialize Terraform: -```bash -terraform init -``` - -4. Review the planned changes: -```bash -terraform plan -``` - -5. Apply the configuration: -```bash -terraform apply -``` ## Security Notes @@ -190,18 +165,6 @@ terraform apply - **IAM**: Roles and policies for ECS tasks and secrets access - **CloudWatch**: Log groups for monitoring -## Variables Reference - -Required variables: -- `provider_id`: Your unique provider identifier -- `local_db_user`: Database username -- `local_db_password`: Database password -- `local_wallet_json`: Wallet JSON (either direct or via file) - -Optional variables (with defaults): -- `aws_region`: AWS region (default: us-east-1) -- `db_name`: Database name (default: orchestrator_db) -- `secrets_prefix`: Prefix for secrets (default: /orchestrator) [Main docs](../README.md) \ No newline at end of file From d72b3ed53b873153c08f4f8fc1f25d899c4c9a15 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 3 Feb 2025 09:32:09 -0500 Subject: [PATCH 22/80] image fix hotfix --- docker-compose/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index b002fb5..d39b66c 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: postgres: - image: postgres:13 + image: postgres:13-alpine environment: POSTGRES_USER: ${DB_USER:-myuser} POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword} From 350e4e63b0b501bf9e27e042cef36ee737f09927 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 3 Feb 2025 09:51:39 -0500 Subject: [PATCH 23/80] added arch --- orchestrator/Dockerfile | 18 +++++++++--------- orchestrator/docs/development.md | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/orchestrator/Dockerfile b/orchestrator/Dockerfile index e0b8740..0ce3931 100644 --- a/orchestrator/Dockerfile +++ b/orchestrator/Dockerfile @@ -1,23 +1,23 @@ -# Use the official Node.js image as the base image -FROM node:20 +# Use the official lightweight Node.js image +FROM node:20-bullseye-slim -# Create and set the working directory +# Create a working directory WORKDIR /usr/src/app -# Copy package.json and package-lock.json +# Copy package files first to leverage Docker layer caching COPY package*.json ./ -# Install dependencies +# Install dependencies including dev dependencies (TypeScript) RUN npm install -# Copy the entire project into the container +# Copy the source files COPY . . -# Compile TypeScript to JavaScript +# Compile TypeScript code RUN npx tsc -# Expose the port if needed +# Expose the app port EXPOSE 3000 -# Run the compiled app +# Run the app CMD ["node", "dist/app.js"] diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 35158c0..fe879a7 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -6,4 +6,28 @@ docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.1.10 . docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -q) -npx ts-node src/clear_outputs.ts \ No newline at end of file +npx ts-node src/clear_outputs.ts + + + + + + + + +docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.2.50 . + +docker login + +docker push randao/orchestrator:latest +docker push randao/orchestrator:v0.2.50 + + +docker buildx create --use +docker buildx inspect --bootstrap + + +docker buildx build --platform linux/amd64,linux/arm64 \ +-t randao/orchestrator:latest \ +-t randao/orchestrator:v0.2.50 \ +--push . From b33b88d489eded9714b8c6d7e121f9bcd66f654e Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 3 Feb 2025 10:58:59 -0500 Subject: [PATCH 24/80] added pulling image --- orchestrator/docs/development.md | 16 +++++--- orchestrator/src/app.ts | 65 ++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index fe879a7..131f9cf 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -15,19 +15,25 @@ npx ts-node src/clear_outputs.ts -docker build -t randao/orchestrator:latest -t randao/orchestrator:v0.2.50 . +# Export version as an environment variable +export VERSION=v0.2.51 # You can change this value to any version you want +# Build the Docker image with the version tag +docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . + +# Log in to Docker docker login +# Push the image with the version tag docker push randao/orchestrator:latest -docker push randao/orchestrator:v0.2.50 - +docker push randao/orchestrator:$VERSION +# Create and use buildx builder docker buildx create --use docker buildx inspect --bootstrap - +# Build the multi-platform image and push it docker buildx build --platform linux/amd64,linux/arm64 \ -t randao/orchestrator:latest \ --t randao/orchestrator:v0.2.50 \ +-t randao/orchestrator:$VERSION \ --push . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 9072c4b..44abce0 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -46,6 +46,7 @@ let PreviousTotalAvailableRandom = 0; // Global variables to track polling status let pollingInProgress = false; let lastPollingId: string | null = null; +let pulledDockerimage = false; // Cache for network configuration let cachedNetworkConfig: NetworkConfig | null = null; @@ -149,6 +150,20 @@ async function triggerVDFJobPod(): Promise { const containerName = `vdf_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; console.log(`Starting Docker container with name: ${containerName}`); try { + if (!pulledDockerimage) { + console.log(`Pulling image: ${VDF_JOB_IMAGE}`); + await new Promise((resolve, reject) => { + docker.pull(VDF_JOB_IMAGE, (err: any, stream: NodeJS.ReadableStream) => { + if (err) { + return reject(err); + } + docker.modem.followProgress(stream, (doneErr) => { + if (doneErr) reject(doneErr); + else resolve(true); + }); + }); + }); + } const container = await docker.createContainer({ Image: VDF_JOB_IMAGE, Cmd: ['sh', '-c', `for i in $(seq 1 ${RANDOM_PER_VDF}); do python main.py; done`], @@ -291,38 +306,38 @@ function updateAvailableValuesAsync(currentCount: number) { async function getMoreRandom(currentCount: number) { const entriesNeeded = MINIMUM_ENTRIES - currentCount; - console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); + console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); - ongoingRequest = true; + ongoingRequest = true; - // Calculate how many containers to spawn - const possibleBatchCount = Math.ceil(entriesNeeded / RANDOM_PER_VDF); - const availableSpawns = Math.min( - possibleBatchCount, - MAX_OUTSTANDING_VDF_CONTAINERS - ongoingContainers.size - ); + // Calculate how many containers to spawn + const possibleBatchCount = Math.ceil(entriesNeeded / RANDOM_PER_VDF); + const availableSpawns = Math.min( + possibleBatchCount, + MAX_OUTSTANDING_VDF_CONTAINERS - ongoingContainers.size + ); - if (availableSpawns <= 0) { - console.log("Max outstanding containers reached. Skipping new container launches."); - ongoingRequest = false; - return; - } + if (availableSpawns <= 0) { + console.log("Max outstanding containers reached. Skipping new container launches."); + ongoingRequest = false; + return; + } - console.log(`Spawning up to ${availableSpawns} containers to generate random values.`); + console.log(`Spawning up to ${availableSpawns} containers to generate random values.`); - for (let i = 0; i < availableSpawns; i++) { - try { - const jobId = await triggerVDFJobPod(); - if (jobId) { - console.log(`Job triggered: ${jobId}`); - ongoingContainers.add(jobId); - } - } catch (error) { - console.error('Error triggering job pod:', error); + for (let i = 0; i < availableSpawns; i++) { + try { + const jobId = await triggerVDFJobPod(); + if (jobId) { + console.log(`Job triggered: ${jobId}`); + ongoingContainers.add(jobId); } + } catch (error) { + console.error('Error triggering job pod:', error); } + } } - + // Function to check and fetch database entries as needed async function checkAndFetchIfNeeded(client: Client) { @@ -361,7 +376,7 @@ async function checkAndFetchIfNeeded(client: Client) { // Check if more entries are needed if (currentCount >= MINIMUM_ENTRIES) return; getMoreRandom(currentCount) - + } catch (error) { console.error('Error during check and fetch:', error); } finally { From 59e4bc693d554f127be67c343f579549ee080993 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 3 Feb 2025 20:36:51 -0500 Subject: [PATCH 25/80] altered IAM policy --- terraform/ecs.tf | 2 +- terraform/iam.json | 87 ++++++++++++++++++++++++++++++ terraform/terraform.tfvars.example | 11 +++- 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 terraform/iam.json diff --git a/terraform/ecs.tf b/terraform/ecs.tf index 0a154fa..93efeb8 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -62,7 +62,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([ { name = "orchestrator" - image = "randao/orchestrator:v0.2.46" + image = "randao/orchestrator:v0.2.51" environment = [ { name = "ENVIRONMENT" diff --git a/terraform/iam.json b/terraform/iam.json new file mode 100644 index 0000000..1150854 --- /dev/null +++ b/terraform/iam.json @@ -0,0 +1,87 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ecs:CreateCluster", + "ecs:DeleteCluster", + "ecs:CreateService", + "ecs:DeleteService", + "ecs:UpdateService", + "ecs:RegisterTaskDefinition", + "ecs:DeregisterTaskDefinition", + "ecs:ListTaskDefinitions", + "ecs:DescribeTaskDefinition", + "ecs:PutClusterCapacityProviders", + "ecs:DescribeClusters" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "iam:CreateRole", + "iam:DeleteRole", + "iam:GetRole", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PassRole" + ], + "Resource": "arn:aws:iam::*:role/orchestrator-*" + }, + { + "Effect": "Allow", + "Action": [ + "secretsmanager:CreateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:GetSecretValue", + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecret", + "secretsmanager:TagResource" + ], + "Resource": "arn:aws:secretsmanager:*:*:secret:/orchestrator/*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:CreateDBInstance", + "rds:DeleteDBInstance", + "rds:ModifyDBInstance", + "rds:DescribeDBInstances", + "rds:CreateDBSubnetGroup", + "rds:DeleteDBSubnetGroup", + "rds:ModifyDBSubnetGroup", + "rds:AddTagsToResource" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:PutRetentionPolicy" + ], + "Resource": "arn:aws:logs:*:*:log-group:*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateSecurityGroup", + "ec2:DeleteSecurityGroup", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress", + "ec2:CreateVpcEndpoint", + "ec2:DeleteVpcEndpoints", + "ec2:DescribeVpcEndpoints", + "ec2:DescribeSecurityGroups", + "ec2:DescribeNetworkInterfaces", + "ec2:CreateTags" + ], + "Resource": "*" + } + ] +} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example index 1c03b11..fcfa02b 100644 --- a/terraform/terraform.tfvars.example +++ b/terraform/terraform.tfvars.example @@ -13,8 +13,15 @@ db_name = "orchestrator_db" # Copy the contents of your wallet.json file and paste it here local_wallet_json = < Date: Mon, 3 Feb 2025 20:37:08 -0500 Subject: [PATCH 26/80] altered IAM policy --- terraform/README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/terraform/README.md b/terraform/README.md index d5d73b8..45e7540 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -20,7 +20,8 @@ "ecs:DeregisterTaskDefinition", "ecs:ListTaskDefinitions", "ecs:DescribeTaskDefinition", - "ecs:PutClusterCapacityProviders" + "ecs:PutClusterCapacityProviders", + "ecs:DescribeClusters" ], "Resource": "*" }, @@ -59,7 +60,8 @@ "rds:DescribeDBInstances", "rds:CreateDBSubnetGroup", "rds:DeleteDBSubnetGroup", - "rds:ModifyDBSubnetGroup" + "rds:ModifyDBSubnetGroup", + "rds:AddTagsToResource" ], "Resource": "*" }, @@ -70,7 +72,7 @@ "logs:DeleteLogGroup", "logs:PutRetentionPolicy" ], - "Resource": "arn:aws:logs:*:*:log-group:/ecs/*" + "Resource": "arn:aws:logs:*:*:log-group:*" }, { "Effect": "Allow", @@ -83,12 +85,14 @@ "ec2:DeleteVpcEndpoints", "ec2:DescribeVpcEndpoints", "ec2:DescribeSecurityGroups", - "ec2:DescribeNetworkInterfaces" + "ec2:DescribeNetworkInterfaces", + "ec2:CreateTags" ], "Resource": "*" } ] -}``` +} +``` Save the JSON and name it Click on Users and create a new user called TeraformDeployer Attach this new policy directly From 3dd7b1e272409e959beb2d3f11f269eac582cc1e Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 4 Feb 2025 17:28:11 -0500 Subject: [PATCH 27/80] modified terraform to leave secrets alone --- docker-compose/docker-compose.yml | 2 +- orchestrator/src/app.ts | 3 ++- terraform/secrets.tf | 13 +++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index d39b66c..1ef18ee 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.2.46 + image: randao/orchestrator:v0.2.51 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 44abce0..af7fb33 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -306,6 +306,7 @@ function updateAvailableValuesAsync(currentCount: number) { async function getMoreRandom(currentCount: number) { const entriesNeeded = MINIMUM_ENTRIES - currentCount; + //TODO this math looked wrong in https://discord.com/channels/1209645894039896074/1333434937537204336/1336000074765045853 console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); ongoingRequest = true; @@ -344,7 +345,7 @@ async function checkAndFetchIfNeeded(client: Client) { try { //Check if provider has been given a special signal const on_chain_avalible_random = await getProviderAvailableRandomValues(PROVIDER_ID); - + //TODO fix so if they are not staked to start it will detect and fix the onchain data and not leave it alone // Query current count of usable DB entries const res = await client.query( 'SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL' diff --git a/terraform/secrets.tf b/terraform/secrets.tf index d85170a..7a89ef4 100644 --- a/terraform/secrets.tf +++ b/terraform/secrets.tf @@ -4,10 +4,14 @@ resource "aws_secretsmanager_secret" "db_credentials" { name = "${var.secrets_prefix}/${var.db_credentials_secret_name}" description = "Database credentials for the orchestrator service" + + lifecycle { + ignore_changes = [name] + } } resource "aws_secretsmanager_secret_version" "db_credentials" { - secret_id = aws_secretsmanager_secret.db_credentials.id + secret_id = aws_secretsmanager_secret.db_credentials.id secret_string = jsonencode({ username = var.local_db_user password = var.local_db_password @@ -18,13 +22,18 @@ resource "aws_secretsmanager_secret_version" "db_credentials" { resource "aws_secretsmanager_secret" "wallet" { name = "${var.secrets_prefix}/${var.wallet_secret_name}" description = "Wallet JSON for the orchestrator service" + + lifecycle { + ignore_changes = [name] + } } resource "aws_secretsmanager_secret_version" "wallet" { - secret_id = aws_secretsmanager_secret.wallet.id + secret_id = aws_secretsmanager_secret.wallet.id secret_string = var.local_wallet_json } + # IAM policy for ECS tasks to access secrets data "aws_iam_policy_document" "secrets_access" { statement { From f167bb75465a42f7a8b54f8cb676073a8ff119df Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 7 Feb 2025 09:29:53 -0500 Subject: [PATCH 28/80] fixed bugs and staking --- orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 131f9cf..46476e0 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.2.51 # You can change this value to any version you want +export VERSION=v0.2.55 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index af7fb33..0647f17 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -35,14 +35,12 @@ const RETRY_DELAY_MS = 10000; const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.4'; const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); -const ongoingTasks = new Set(); // Track task ARNs of running ECS tasks const ongoingContainers = new Set(); // Track container IDs of running Docker containers const PROVIDER_ID = process.env.PROVIDER_ID || "0"; const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds let ongoingRequest = false; let spotInterruptions = 0; -let PreviousTotalAvailableRandom = 0; // Global variables to track polling status let pollingInProgress = false; let lastPollingId: string | null = null; @@ -124,7 +122,7 @@ async function triggerVDFJobPod(): Promise { const taskArn = await launchVDFTask(ecs, cachedNetworkConfig, RANDOM_PER_VDF); if (taskArn) { - ongoingTasks.add(taskArn); + ongoingContainers.add(taskArn); console.log(`ECS task started successfully: ${taskArn}`); return taskArn; } @@ -195,11 +193,11 @@ async function triggerVDFJobPod(): Promise { // Modified function to wait for ECS tasks to complete and remove them from tracking async function monitorECSTasks(): Promise { - if (ongoingTasks.size === 0) return; + if (ongoingContainers.size === 0) return; const describeTasksResult = await ecs.describeTasks({ cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - tasks: Array.from(ongoingTasks) + tasks: Array.from(ongoingContainers) }).promise(); describeTasksResult.tasks?.forEach(task => { @@ -219,7 +217,7 @@ async function monitorECSTasks(): Promise { } else { console.log('Task stopped due to normal completion.'); } - ongoingTasks.delete(task.taskArn as string); + ongoingContainers.delete(task.taskArn as string); } }); } @@ -345,7 +343,6 @@ async function checkAndFetchIfNeeded(client: Client) { try { //Check if provider has been given a special signal const on_chain_avalible_random = await getProviderAvailableRandomValues(PROVIDER_ID); - //TODO fix so if they are not staked to start it will detect and fix the onchain data and not leave it alone // Query current count of usable DB entries const res = await client.query( 'SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL' @@ -366,10 +363,9 @@ async function checkAndFetchIfNeeded(client: Client) { break; default: console.log("Value is not -1, -2, or -3"); - if (PreviousTotalAvailableRandom !== currentCount) { - console.log(`Updating available random values from ${PreviousTotalAvailableRandom} to ${currentCount}`); + if (on_chain_avalible_random.availibleRandomValues !== currentCount) { + console.log(`Updating available random values from ${on_chain_avalible_random.availibleRandomValues} to ${currentCount}`); updateAvailableValuesAsync(currentCount); - PreviousTotalAvailableRandom = currentCount; } } if (ongoingRequest) return; // Prevent redundant operations @@ -456,7 +452,7 @@ function getLogId(): string { return `[LogID: ${randomId} | ${timestamp}]`; } -async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { +async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { let openRequests: GetOpenRandomRequestsResponse; @@ -487,13 +483,15 @@ async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Pr openRequests = await fetchOpenRequests(); // Retry request } + if(openRequests.toString().includes("not found")){ + return false + } console.log(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(openRequests)}`); console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeChallengeRequests.request_ids.length}`); console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeOutputRequests.request_ids.length}`); return openRequests; } -//Todo timeout the second anddefault to 0 async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise { let avalibleRandom: GetProviderAvailableValuesResponse; // Create a function to fetch open requests with a timeout @@ -552,6 +550,10 @@ async function polling(client: any) { const s1 = Date.now(); console.log(`${logId} Step 1 started.`); const openRequests = await getProviderRequests(PROVIDER_ID, logId); + if(openRequests == false){ + console.log("Provider is set up and ready. Please stake to join network at https://providers_randao.ar.io") + return + } stepTracking.step1 = { completed: true, timeTaken: Date.now() - s1 }; console.log(`${logId} Step 1: Open requests fetched. Time taken: ${stepTracking.step1.timeTaken}ms`); From a7e7e275ad908e9e77a47146a0074da8b0757cae Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 7 Feb 2025 10:06:03 -0500 Subject: [PATCH 29/80] fixed up some stuff --- docker-compose/dev-docker-compose.yml | 2 +- docker-compose/docker-compose.yml | 2 +- requester/package.json | 2 +- requester/src/app.ts | 100 ++++++++++++++++++++------ terraform/ecs.tf | 2 +- 5 files changed, 81 insertions(+), 27 deletions(-) diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml index 5ed36c1..0069344 100644 --- a/docker-compose/dev-docker-compose.yml +++ b/docker-compose/dev-docker-compose.yml @@ -133,7 +133,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.2.1 + image: randao/requester:v0.2.7 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 1ef18ee..a9bcffa 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.2.51 + image: randao/orchestrator:v0.2.55 depends_on: postgres: condition: service_healthy diff --git a/requester/package.json b/requester/package.json index 798686b..a2e12d5 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "3.5.17", + "ao-process-clients": "3.7.3", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/requester/src/app.ts b/requester/src/app.ts index 142f2d2..673c16c 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -3,18 +3,17 @@ import { IRandomClient, RandomClient, RandomClientConfig, + StakingClient, } from "ao-process-clients"; -const PROVIDER_IDS = [ - "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE", - "c8Iq4yunDnsJWGSz_wYwQU--O9qeODKHiRdUkQkW2p8", - "Sr3HVH0Nh6iZzbORLpoQFOEvmsuKjXsHswSWH760KAk", - "1zlA7nKecUGevGNAEbjim_SlbioOI6daNNn2luDEHb0" -]; - -const RETRY_DELAY_MS = 5000; //5 seconds +const RETRY_DELAY_MS = 1000; // 1 second +const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes +const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; +let cachedProviders: string[] = []; +let lastProviderRefresh = 0; + const RANDOM_CONFIG: RandomClientConfig = { tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", @@ -27,23 +26,78 @@ let totalTimeToFulfill = 0; let fulfilledRequests = 0; const outstandingRequests: Set = new Set(); -function getRandomProviders(): { providers: string[], count: number } { - // Randomly select how many providers we want (1-3) - const count = Math.floor(Math.random() * 3) + 1; - - // Shuffle the provider array and take the first 'count' elements - const shuffled = [...PROVIDER_IDS] - .sort(() => Math.random() - 0.5) - .slice(0, Math.min(count, PROVIDER_IDS.length)); +async function getRandomProviders(stakeclient: StakingClient): Promise<{ providers: string[], count: number }> { + const now = Date.now(); - return { - providers: shuffled, - count: shuffled.length - }; + // If we have cached providers and they're not expired, use them + if (cachedProviders.length > 0 && (now - lastProviderRefresh) < PROVIDER_REFRESH_INTERVAL) { + const count = Math.floor(Math.random() * 3) + 1; + const shuffled = [...cachedProviders] + .sort(() => Math.random() - 0.5) + .slice(0, Math.min(count, cachedProviders.length)); + return { + providers: shuffled, + count: shuffled.length + }; + } + + try { + // Create a promise that rejects after timeout + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("Provider request timed out")), PROVIDER_REQUEST_TIMEOUT); + }); + + // Create the actual provider fetch promise + const fetchPromise = async () => { + const providerInfo = await stakeclient.getAllProvidersInfo(); + const eligibleProviders = providerInfo + .filter(provider => provider.active === 1) + .map(provider => provider.provider_id); + + if (eligibleProviders.length === 0) { + throw new Error("No eligible providers found with active status"); + } + + // Update cache + cachedProviders = eligibleProviders; + lastProviderRefresh = now; + + const count = Math.floor(Math.random() * 3) + 1; + const shuffled = [...eligibleProviders] + .sort(() => Math.random() - 0.5) + .slice(0, Math.min(count, eligibleProviders.length)); + + return { + providers: shuffled, + count: shuffled.length + }; + }; + + // Race between timeout and fetch + return await Promise.race([fetchPromise(), timeoutPromise]); + } catch (error) { + console.error("Error fetching providers:", error); + + // If we have cached providers, use them as fallback + if (cachedProviders.length > 0) { + console.log("Using cached providers as fallback"); + const count = Math.floor(Math.random() * 3) + 1; + const shuffled = [...cachedProviders] + .sort(() => Math.random() - 0.5) + .slice(0, Math.min(count, cachedProviders.length)); + return { + providers: shuffled, + count: shuffled.length + }; + } + + throw error; // Re-throw if we have no fallback + } } async function main() { - const randclient: IRandomClient = new RandomClient(RANDOM_CONFIG); + const randclient = new RandomClient(RANDOM_CONFIG); + const stakeclient = new StakingClient(RANDOM_CONFIG); while (true) { console.log("Running") @@ -52,7 +106,7 @@ async function main() { if (Math.random() < CHANCE_TO_CALL_RANDOM) { console.log("Initiating random request..."); const callbackId = `callback-${Date.now()}`; - const { providers, count } = getRandomProviders(); + const { providers, count } = await getRandomProviders(stakeclient); console.log(`Selected ${count} providers:`, providers); await randclient.createRequest(providers, count, callbackId); totalRandomCalled++; @@ -118,4 +172,4 @@ function delay(ms: number): Promise { } // Call the main function -main(); \ No newline at end of file +main(); diff --git a/terraform/ecs.tf b/terraform/ecs.tf index 93efeb8..ed8e315 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -62,7 +62,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([ { name = "orchestrator" - image = "randao/orchestrator:v0.2.51" + image = "randao/orchestrator:v0.2.55" environment = [ { name = "ENVIRONMENT" From e7d8ca5a0f0b688e3b748ad23c6746121e26004d Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 26 Feb 2025 05:37:56 -0500 Subject: [PATCH 30/80] New code --- docker-compose/dev-docker-compose.yml | 6 +- docker-compose/docker-compose.yml | 6 +- docker-compose/mass-dev-docker-compose.yml | 645 +++++++++++++++++++++ orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 59 +- orchestrator/src/clear_outputs.ts | 26 +- orchestrator/src/stake.ts | 26 +- terraform/ecs.tf | 2 +- 9 files changed, 727 insertions(+), 47 deletions(-) create mode 100644 docker-compose/mass-dev-docker-compose.yml diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml index 0069344..9789769 100644 --- a/docker-compose/dev-docker-compose.yml +++ b/docker-compose/dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.2.46 + image: randao/orchestrator:v0.2.80 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.2.46 + image: randao/orchestrator:v0.2.80 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.2.46 + image: randao/orchestrator:v0.2.80 depends_on: postgres3: condition: service_healthy diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index a9bcffa..e93ca33 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: postgres: image: postgres:13-alpine @@ -8,7 +6,7 @@ services: POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword} POSTGRES_DB: ${DB_NAME:-mydatabase} ports: - - "5432:5432" + - "5431:5432" networks: - backend volumes: @@ -20,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.2.55 + image: randao/orchestrator:v0.3.0 depends_on: postgres: condition: service_healthy diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml new file mode 100644 index 0000000..7e5c4e1 --- /dev/null +++ b/docker-compose/mass-dev-docker-compose.yml @@ -0,0 +1,645 @@ +version: '3.8' + +services: + # Instance 1 + postgres1: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_1:-myuser1} + POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword1} + POSTGRES_DB: ${DB_NAME_1:-mydatabase1} + ports: + - "5432:5432" + networks: + - backend + volumes: + - pgdata1:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser1} -d ${DB_NAME_1:-mydatabase1}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator1: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres1: + condition: service_healthy + environment: + DB_HOST: postgres1 + DB_PORT: 5432 + DB_USER: ${DB_USER_1:-myuser1} + DB_PASSWORD: ${DB_PASSWORD_1:-mypassword1} + DB_NAME: ${DB_NAME_1:-mydatabase1} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_1} + PROVIDER_ID: ${PROVIDER_ID_1} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 2 + postgres2: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_2:-myuser2} + POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword2} + POSTGRES_DB: ${DB_NAME_2:-mydatabase2} + ports: + - "5433:5432" + networks: + - backend + volumes: + - pgdata2:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser2} -d ${DB_NAME_2:-mydatabase2}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator2: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres2: + condition: service_healthy + environment: + DB_HOST: postgres2 + DB_PORT: 5432 + DB_USER: ${DB_USER_2:-myuser2} + DB_PASSWORD: ${DB_PASSWORD_2:-mypassword2} + DB_NAME: ${DB_NAME_2:-mydatabase2} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_2} + PROVIDER_ID: ${PROVIDER_ID_2} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 3 + postgres3: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_3:-myuser3} + POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword3} + POSTGRES_DB: ${DB_NAME_3:-mydatabase3} + ports: + - "5434:5432" + networks: + - backend + volumes: + - pgdata3:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser3} -d ${DB_NAME_3:-mydatabase3}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator3: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres3: + condition: service_healthy + environment: + DB_HOST: postgres3 + DB_PORT: 5432 + DB_USER: ${DB_USER_3:-myuser3} + DB_PASSWORD: ${DB_PASSWORD_3:-mypassword3} + DB_NAME: ${DB_NAME_3:-mydatabase3} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_3} + PROVIDER_ID: ${PROVIDER_ID_3} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 4 + postgres4: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_4:-myuser4} + POSTGRES_PASSWORD: ${DB_PASSWORD_4:-mypassword4} + POSTGRES_DB: ${DB_NAME_4:-mydatabase4} + ports: + - "5435:5432" + networks: + - backend + volumes: + - pgdata4:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_4:-myuser4} -d ${DB_NAME_4:-mydatabase4}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator4: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres4: + condition: service_healthy + environment: + DB_HOST: postgres4 + DB_PORT: 5432 + DB_USER: ${DB_USER_4:-myuser4} + DB_PASSWORD: ${DB_PASSWORD_4:-mypassword4} + DB_NAME: ${DB_NAME_4:-mydatabase4} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_4} + PROVIDER_ID: ${PROVIDER_ID_4} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 5 + postgres5: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_5:-myuser5} + POSTGRES_PASSWORD: ${DB_PASSWORD_5:-mypassword5} + POSTGRES_DB: ${DB_NAME_5:-mydatabase5} + ports: + - "5436:5432" + networks: + - backend + volumes: + - pgdata5:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_5:-myuser5} -d ${DB_NAME_5:-mydatabase5}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator5: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres5: + condition: service_healthy + environment: + DB_HOST: postgres5 + DB_PORT: 5432 + DB_USER: ${DB_USER_5:-myuser5} + DB_PASSWORD: ${DB_PASSWORD_5:-mypassword5} + DB_NAME: ${DB_NAME_5:-mydatabase5} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_5} + PROVIDER_ID: ${PROVIDER_ID_5} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 6 + postgres6: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_6:-myuser6} + POSTGRES_PASSWORD: ${DB_PASSWORD_6:-mypassword6} + POSTGRES_DB: ${DB_NAME_6:-mydatabase6} + ports: + - "5437:5432" + networks: + - backend + volumes: + - pgdata6:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_6:-myuser6} -d ${DB_NAME_6:-mydatabase6}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator6: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres6: + condition: service_healthy + environment: + DB_HOST: postgres6 + DB_PORT: 5432 + DB_USER: ${DB_USER_6:-myuser6} + DB_PASSWORD: ${DB_PASSWORD_6:-mypassword6} + DB_NAME: ${DB_NAME_6:-mydatabase6} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_6} + PROVIDER_ID: ${PROVIDER_ID_6} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 7 + postgres7: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_7:-myuser7} + POSTGRES_PASSWORD: ${DB_PASSWORD_7:-mypassword7} + POSTGRES_DB: ${DB_NAME_7:-mydatabase7} + ports: + - "5438:5432" + networks: + - backend + volumes: + - pgdata7:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_7:-myuser7} -d ${DB_NAME_7:-mydatabase7}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator7: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres7: + condition: service_healthy + environment: + DB_HOST: postgres7 + DB_PORT: 5432 + DB_USER: ${DB_USER_7:-myuser7} + DB_PASSWORD: ${DB_PASSWORD_7:-mypassword7} + DB_NAME: ${DB_NAME_7:-mydatabase7} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_7} + PROVIDER_ID: ${PROVIDER_ID_7} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 8 + postgres8: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_8:-myuser8} + POSTGRES_PASSWORD: ${DB_PASSWORD_8:-mypassword8} + POSTGRES_DB: ${DB_NAME_8:-mydatabase8} + ports: + - "5439:5432" + networks: + - backend + volumes: + - pgdata8:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_8:-myuser8} -d ${DB_NAME_8:-mydatabase8}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator8: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres8: + condition: service_healthy + environment: + DB_HOST: postgres8 + DB_PORT: 5432 + DB_USER: ${DB_USER_8:-myuser8} + DB_PASSWORD: ${DB_PASSWORD_8:-mypassword8} + DB_NAME: ${DB_NAME_8:-mydatabase8} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_8} + PROVIDER_ID: ${PROVIDER_ID_8} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 9 + postgres9: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_9:-myuser9} + POSTGRES_PASSWORD: ${DB_PASSWORD_9:-mypassword9} + POSTGRES_DB: ${DB_NAME_9:-mydatabase9} + ports: + - "5440:5432" + networks: + - backend + volumes: + - pgdata9:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_9:-myuser9} -d ${DB_NAME_9:-mydatabase9}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator9: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres9: + condition: service_healthy + environment: + DB_HOST: postgres9 + DB_PORT: 5432 + DB_USER: ${DB_USER_9:-myuser9} + DB_PASSWORD: ${DB_PASSWORD_9:-mypassword9} + DB_NAME: ${DB_NAME_9:-mydatabase9} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_9} + PROVIDER_ID: ${PROVIDER_ID_9} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 10 + postgres10: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_10:-myuser10} + POSTGRES_PASSWORD: ${DB_PASSWORD_10:-mypassword10} + POSTGRES_DB: ${DB_NAME_10:-mydatabase10} + ports: + - "5441:5432" + networks: + - backend + volumes: + - pgdata10:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_10:-myuser10} -d ${DB_NAME_10:-mydatabase10}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator10: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres10: + condition: service_healthy + environment: + DB_HOST: postgres10 + DB_PORT: 5432 + DB_USER: ${DB_USER_10:-myuser10} + DB_PASSWORD: ${DB_PASSWORD_10:-mypassword10} + DB_NAME: ${DB_NAME_10:-mydatabase10} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_10} + PROVIDER_ID: ${PROVIDER_ID_10} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 11 + postgres11: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_11:-myuser11} + POSTGRES_PASSWORD: ${DB_PASSWORD_11:-mypassword11} + POSTGRES_DB: ${DB_NAME_11:-mydatabase11} + ports: + - "5442:5432" + networks: + - backend + volumes: + - pgdata11:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_11:-myuser11} -d ${DB_NAME_11:-mydatabase11}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator11: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres11: + condition: service_healthy + environment: + DB_HOST: postgres11 + DB_PORT: 5432 + DB_USER: ${DB_USER_11:-myuser11} + DB_PASSWORD: ${DB_PASSWORD_11:-mypassword11} + DB_NAME: ${DB_NAME_11:-mydatabase11} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_11} + PROVIDER_ID: ${PROVIDER_ID_11} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 12 + postgres12: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_12:-myuser12} + POSTGRES_PASSWORD: ${DB_PASSWORD_12:-mypassword12} + POSTGRES_DB: ${DB_NAME_12:-mydatabase12} + ports: + - "5443:5432" + networks: + - backend + volumes: + - pgdata12:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_12:-myuser12} -d ${DB_NAME_12:-mydatabase12}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator12: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres12: + condition: service_healthy + environment: + DB_HOST: postgres12 + DB_PORT: 5432 + DB_USER: ${DB_USER_12:-myuser12} + DB_PASSWORD: ${DB_PASSWORD_12:-mypassword12} + DB_NAME: ${DB_NAME_12:-mydatabase12} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_12} + PROVIDER_ID: ${PROVIDER_ID_12} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 13 + postgres13: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_13:-myuser13} + POSTGRES_PASSWORD: ${DB_PASSWORD_13:-mypassword13} + POSTGRES_DB: ${DB_NAME_13:-mydatabase13} + ports: + - "5444:5432" + networks: + - backend + volumes: + - pgdata13:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_13:-myuser13} -d ${DB_NAME_13:-mydatabase13}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator13: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres13: + condition: service_healthy + environment: + DB_HOST: postgres13 + DB_PORT: 5432 + DB_USER: ${DB_USER_13:-myuser13} + DB_PASSWORD: ${DB_PASSWORD_13:-mypassword13} + DB_NAME: ${DB_NAME_13:-mydatabase13} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_13} + PROVIDER_ID: ${PROVIDER_ID_13} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 14 + postgres14: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_14:-myuser14} + POSTGRES_PASSWORD: ${DB_PASSWORD_14:-mypassword14} + POSTGRES_DB: ${DB_NAME_14:-mydatabase14} + ports: + - "5445:5432" + networks: + - backend + volumes: + - pgdata14:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_14:-myuser14} -d ${DB_NAME_14:-mydatabase14}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator14: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres14: + condition: service_healthy + environment: + DB_HOST: postgres14 + DB_PORT: 5432 + DB_USER: ${DB_USER_14:-myuser14} + DB_PASSWORD: ${DB_PASSWORD_14:-mypassword14} + DB_NAME: ${DB_NAME_14:-mydatabase14} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_14} + PROVIDER_ID: ${PROVIDER_ID_14} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Instance 15 + postgres15: + image: postgres:13 + environment: + POSTGRES_USER: ${DB_USER_15:-myuser15} + POSTGRES_PASSWORD: ${DB_PASSWORD_15:-mypassword15} + POSTGRES_DB: ${DB_NAME_15:-mydatabase15} + ports: + - "5446:5432" + networks: + - backend + volumes: + - pgdata15:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER_15:-myuser15} -d ${DB_NAME_15:-mydatabase15}"] + interval: 10s + timeout: 5s + retries: 5 + + orchestrator15: + image: randao/orchestrator:v0.2.80 + depends_on: + postgres15: + condition: service_healthy + environment: + DB_HOST: postgres15 + DB_PORT: 5432 + DB_USER: ${DB_USER_15:-myuser15} + DB_PASSWORD: ${DB_PASSWORD_15:-mypassword15} + DB_NAME: ${DB_NAME_15:-mydatabase15} + ENVIRONMENT: local + WALLET_JSON: ${WALLET_JSON_15} + PROVIDER_ID: ${PROVIDER_ID_15} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # Single DBeaver Instance + dbeaver: + image: dbeaver/cloudbeaver:23.2.0 + environment: + CB_SERVER_SERVER_PORT: 8080 + CB_SERVER_ADMIN_NAME: "admin" + CB_SERVER_ADMIN_PASSWORD: "admin123" + networks: + - backend + ports: + - "8080:8978" + volumes: + - dbeaver-data:/opt/cloudbeaver/workspace + + requester: + image: randao/requester:v0.2.7 + environment: + REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} + +networks: + backend: + name: backend + driver: bridge + +volumes: + pgdata1: + driver: local + pgdata2: + driver: local + pgdata3: + driver: local + pgdata4: + driver: local + pgdata5: + driver: local + pgdata6: + driver: local + pgdata7: + driver: local + pgdata8: + driver: local + pgdata9: + driver: local + pgdata10: + driver: local + pgdata11: + driver: local + pgdata12: + driver: local + pgdata13: + driver: local + pgdata14: + driver: local + pgdata15: + driver: local + dbeaver-data: + driver: local diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 46476e0..411b276 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.2.55 # You can change this value to any version you want +export VERSION=v0.3.0 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 959d2ce..34df87e 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients":"3.6.0", + "ao-process-clients":"5.2.8", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 0647f17..ad37d28 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,26 +1,44 @@ import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; -import { Environment, GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig } from "ao-process-clients" +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RandomClientConfig} from "ao-process-clients" import { dbConfig } from './db_config.js'; import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; +// const RANDOM_CONFIG: RandomClientConfig = { +// wallet: JSON.parse(process.env.WALLET_JSON!), +// tokenProcessId: '', +// processId: '' +// } +//const randclient: IRandomClient = RandomClient.autoConfiguration() +async function getRandomClient(): Promise{ +// let test = await getRandomClientAutoConfiguration() +// test.wallet = JSON.parse(process.env.WALLET_JSON!) + const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", - processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", wallet: JSON.parse(process.env.WALLET_JSON!), - environment: 'mainnet' + tokenProcessId: '5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI', + processId: '1dnDvaDRQ7Ao6o1ohTr7NNrN5mp1CpsXFrWm3JJFEs8' } -//const randclient: IRandomClient = RandomClient.autoConfiguration() const randclient = new RandomClient(RANDOM_CONFIG) + return randclient +} + +// async function getStakingClient(): Promise{ +// let test = await getProviderStakingClientAutoConfiguration() +// test.wallet = JSON.parse(process.env.WALLET_JSON!) +// const randclient = new ProviderStakingClient(test) +// return randclient +// } + const docker = new Docker(); // Constants for configuration -const POLLING_INTERVAL_MS = 5000; +const POLLING_INTERVAL_MS = 1000; const MINIMUM_ENTRIES = 500; const DRYRUNTIMEOUT = 15000; // 15 seconds const DRYRUNRESETTIME = 300000; // 5 min @@ -161,6 +179,7 @@ async function triggerVDFJobPod(): Promise { }); }); }); + pulledDockerimage = true; } const container = await docker.createContainer({ Image: VDF_JOB_IMAGE, @@ -294,7 +313,7 @@ function addHexPrefix(value: string): string { function updateAvailableValuesAsync(currentCount: number) { return (async () => { try { - await randclient.updateProviderAvailableValues(currentCount); + (await getRandomClient()).updateProviderAvailableValues(currentCount); console.log(`Updated provider values to ${currentCount}`); } catch (error) { console.error("Failed to update provider values:", error); @@ -354,6 +373,7 @@ async function checkAndFetchIfNeeded(client: Client) { case -1: console.log("Provider is shutting down"); //TODO prepare for shutdown + //TODO the async causes it to ovewrite itself break; case -2: console.log("Value is -2"); @@ -363,7 +383,8 @@ async function checkAndFetchIfNeeded(client: Client) { break; default: console.log("Value is not -1, -2, or -3"); - if (on_chain_avalible_random.availibleRandomValues !== currentCount) { + console.log("Value is "+ on_chain_avalible_random.availibleRandomValues) + if (on_chain_avalible_random.availibleRandomValues != currentCount) { console.log(`Updating available random values from ${on_chain_avalible_random.availibleRandomValues} to ${currentCount}`); updateAvailableValuesAsync(currentCount); } @@ -406,7 +427,7 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL const hexInput = addHexPrefix(input); console.log(`${parentLogId} Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); - await randclient.postVDFChallenge(requestId, hexModulus, hexInput); + await (await getRandomClient()).postVDFChallenge(requestId, hexModulus, hexInput); console.log(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); } catch (error) { console.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); @@ -439,7 +460,7 @@ async function fulfillRandomOutput(client: Client, requestId: string, parentLogI const proofString = JSON.stringify(processedProof); console.log(`${parentLogId} Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); - await randclient.postVDFOutputAndProof(requestId, processedOutput, proofString); + await (await getRandomClient()).postVDFOutputAndProof(requestId, processedOutput, proofString); console.log(`${parentLogId} Proof posted for request ID: ${requestId}`); } catch (error) { console.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); @@ -459,7 +480,7 @@ async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Pr // Create a function to fetch open requests with a timeout const fetchOpenRequests = async (): Promise => { try { - const response = await randclient.getOpenRandomRequests(PROVIDER_ID); + const response = await (await getRandomClient()).getOpenRandomRequests(PROVIDER_ID); return response; } catch (error) { console.error(`${parentLogId} Error fetching requests: ${error}`); @@ -478,7 +499,8 @@ async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Pr ]); } catch (error) { console.log(`${parentLogId} Step 1: ${error}`); - randclient.setDryRunAsMessage(true); + //randclient.setDryRunAsMessage(true); + console.log("Removed this as its spamming") console.log("Switching dryrun off"); openRequests = await fetchOpenRequests(); // Retry request } @@ -497,7 +519,7 @@ async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise => { try { - const response = await randclient.getProviderAvailableValues(PROVIDER_ID); + const response = await (await getRandomClient()).getProviderAvailableValues(PROVIDER_ID); return response; } catch (error) { console.error(`Error fetching avalible random: ${error}`); @@ -516,7 +538,8 @@ async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise { await polling(client); }, POLLING_INTERVAL_MS); - setInterval(async () => { - randclient.setDryRunAsMessage(false); - console.log("Switching dryrun on") - }, DRYRUNRESETTIME); + // setInterval(async () => { + // randclient.setDryRunAsMessage(false); + // console.log("Switching dryrun on") + // }, DRYRUNRESETTIME); process.on("SIGTERM", async () => { diff --git a/orchestrator/src/clear_outputs.ts b/orchestrator/src/clear_outputs.ts index a554e4d..88772fc 100644 --- a/orchestrator/src/clear_outputs.ts +++ b/orchestrator/src/clear_outputs.ts @@ -1,16 +1,20 @@ import { Client } from 'pg'; -import { getRandomClientAutoConfiguration, RandomClient, RandomClientConfig } from "ao-process-clients"; +import { RandomClient, RandomClientConfig } from "ao-process-clients"; import { dbConfig } from './db_config'; // Random Client Configuration -const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", - processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", - wallet: JSON.parse(process.env.WALLET_JSON!), - environment: 'mainnet' -} - -const randclient = new RandomClient(RANDOM_CONFIG); +async function getRandomClient(): Promise{ + // let test = await getRandomClientAutoConfiguration() + // test.wallet = JSON.parse(process.env.WALLET_JSON!) + + const RANDOM_CONFIG: RandomClientConfig = { + wallet: JSON.parse(process.env.WALLET_JSON!), + tokenProcessId: '5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI', + processId: '1dnDvaDRQ7Ao6o1ohTr7NNrN5mp1CpsXFrWm3JJFEs8' + } + const randclient = new RandomClient(RANDOM_CONFIG) + return randclient + } const PROVIDER_ID = process.env.PROVIDER_ID || "0"; // Function to connect to PostgreSQL @@ -25,7 +29,7 @@ async function connectToDatabase() { async function clearAllOutputRequests(client: Client) { try { console.log("Fetching open output requests..."); - const openRequests = await randclient.getOpenRandomRequests(PROVIDER_ID); + const openRequests = await (await getRandomClient()).getOpenRandomRequests(PROVIDER_ID); if (openRequests && openRequests.activeOutputRequests) { console.log(`Found ${openRequests.activeOutputRequests.request_ids.length} output requests to clear.`); @@ -34,7 +38,7 @@ async function clearAllOutputRequests(client: Client) { const clearPromises = openRequests.activeOutputRequests.request_ids.map(async (requestId: string) => { console.log(`Sending "No data" for output request ID: ${requestId}`); try { - await randclient.postVDFOutputAndProof(requestId, "No data", "No data"); + await (await getRandomClient()).postVDFOutputAndProof(requestId, "No data", "No data"); console.log(`"No data" successfully sent for request ID: ${requestId}`); } catch (error) { console.error(`Error sending "No data" for request ID: ${requestId}:`, error); diff --git a/orchestrator/src/stake.ts b/orchestrator/src/stake.ts index 9ed352f..862b81c 100644 --- a/orchestrator/src/stake.ts +++ b/orchestrator/src/stake.ts @@ -1,15 +1,25 @@ -import { getRandomClientAutoConfiguration, RandomClient, RandomClientConfig, StakingClient, } from "ao-process-clients"; -import { ProviderDetails } from "ao-process-clients/dist/src/clients/staking/abstract/types"; +import { ProviderDetails, ProviderStakingClient, StakingClientConfig } from "ao-process-clients"; // Random Client Configuration -const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", - processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", +// async function getStakingClient(): Promise{ +// let test = await getProviderStakingClientAutoConfiguration() +// test.wallet = JSON.parse(process.env.WALLET_JSON!) +// const randclient = new ProviderStakingClient(test) +// return randclient +// } + +async function getStakingClient(): Promise{ +// let test = await getProviderStakingClientAutoConfiguration() +// test.wallet = JSON.parse(process.env.WALLET_JSON!) +const RANDOM_CONFIG: StakingClientConfig = { wallet: JSON.parse(process.env.WALLET_JSON!), - environment: 'mainnet' + tokenProcessId: '5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI', + processId: 'EIQJoqVWonlxsEe8xGpQZhh54wrmgE3q0tAsVIhKYQU' +} +const randclient = new ProviderStakingClient(RANDOM_CONFIG) + return randclient } -const randclient = new StakingClient(RANDOM_CONFIG); // Function to clear all output requests async function stake() { @@ -26,7 +36,7 @@ async function stake() { discord: "test_discord", /** Optional Telegram handle */ telegram: "test_tg"}; - console.log(await randclient.stake("100000000000000000000",providerDetails)) + console.log(await (await getStakingClient()).stakeWithDetails("100000000000000000000",providerDetails)) } catch (error) { console.error("An error occurred while staking:", error); } finally { diff --git a/terraform/ecs.tf b/terraform/ecs.tf index ed8e315..c57785a 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -62,7 +62,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([ { name = "orchestrator" - image = "randao/orchestrator:v0.2.55" + image = "randao/orchestrator:v0.3.0" environment = [ { name = "ENVIRONMENT" From f24dd1661dd8b0d77bce50a043eaa27985635f01 Mon Sep 17 00:00:00 2001 From: tendiehelpe Date: Thu, 27 Feb 2025 11:58:12 -0500 Subject: [PATCH 31/80] provider upgrade --- docker-compose/docker-compose.yml | 2 +- docker-compose/mass-dev-docker-compose.yml | 30 +++++++++++----------- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 4 ++- orchestrator/src/app.ts | 27 ++++++++++++++++--- requester/package.json | 3 ++- terraform/ecs.tf | 2 +- 7 files changed, 47 insertions(+), 23 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index e93ca33..3d7b40c 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.3.0 + image: randao/orchestrator:v0.3.2 depends_on: postgres: condition: service_healthy diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml index 7e5c4e1..2da3ea1 100644 --- a/docker-compose/mass-dev-docker-compose.yml +++ b/docker-compose/mass-dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres3: condition: service_healthy @@ -138,7 +138,7 @@ services: retries: 5 orchestrator4: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres4: condition: service_healthy @@ -177,7 +177,7 @@ services: retries: 5 orchestrator5: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres5: condition: service_healthy @@ -216,7 +216,7 @@ services: retries: 5 orchestrator6: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres6: condition: service_healthy @@ -255,7 +255,7 @@ services: retries: 5 orchestrator7: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres7: condition: service_healthy @@ -294,7 +294,7 @@ services: retries: 5 orchestrator8: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres8: condition: service_healthy @@ -333,7 +333,7 @@ services: retries: 5 orchestrator9: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres9: condition: service_healthy @@ -372,7 +372,7 @@ services: retries: 5 orchestrator10: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres10: condition: service_healthy @@ -411,7 +411,7 @@ services: retries: 5 orchestrator11: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres11: condition: service_healthy @@ -450,7 +450,7 @@ services: retries: 5 orchestrator12: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres12: condition: service_healthy @@ -489,7 +489,7 @@ services: retries: 5 orchestrator13: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres13: condition: service_healthy @@ -528,7 +528,7 @@ services: retries: 5 orchestrator14: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres14: condition: service_healthy @@ -567,7 +567,7 @@ services: retries: 5 orchestrator15: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.2 depends_on: postgres15: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 411b276..f2fb4ba 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.3.0 # You can change this value to any version you want +export VERSION=v0.3.1 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 34df87e..1d32dd4 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,9 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients":"5.2.8", + "ao-process-clients": "5.2.8", + "ao-vrf": "file:", + "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index ad37d28..18c191d 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -4,7 +4,7 @@ import AWS from 'aws-sdk'; import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RandomClientConfig} from "ao-process-clients" import { dbConfig } from './db_config.js'; import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; - +import Arweave from 'arweave'; // const RANDOM_CONFIG: RandomClientConfig = { // wallet: JSON.parse(process.env.WALLET_JSON!), @@ -41,7 +41,7 @@ const docker = new Docker(); const POLLING_INTERVAL_MS = 1000; const MINIMUM_ENTRIES = 500; const DRYRUNTIMEOUT = 15000; // 15 seconds -const DRYRUNRESETTIME = 300000; // 5 min +//const DRYRUNRESETTIME = 300000; // 5 min //Expected increments per second=10×0.005=0.05 //180 times per hour //4,320 times per day @@ -54,7 +54,7 @@ const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.4'; const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); const ongoingContainers = new Set(); // Track container IDs of running Docker containers -const PROVIDER_ID = process.env.PROVIDER_ID || "0"; +let PROVIDER_ID = ""; const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds let ongoingRequest = false; @@ -68,6 +68,8 @@ let pulledDockerimage = false; let cachedNetworkConfig: NetworkConfig | null = null; +const arweave = Arweave.init({}); + interface StepTracking { step1?: { completed: boolean; timeTaken: number }; step2?: { completed: boolean; timeTaken: number }; @@ -321,6 +323,18 @@ function updateAvailableValuesAsync(currentCount: number) { })(); } +async function shutdown() { + return (async () => { + try { + let message = await (await getRandomClient()).updateProviderAvailableValues(0); + console.log(message) + console.log(`Updated provider values to ${0}`); + } catch (error) { + console.error("Failed to update provider values:", error); + } + })(); +} + async function getMoreRandom(currentCount: number) { const entriesNeeded = MINIMUM_ENTRIES - currentCount; //TODO this math looked wrong in https://discord.com/channels/1209645894039896074/1333434937537204336/1336000074765045853 @@ -810,6 +824,12 @@ async function run(): Promise { const client = await connectWithRetry(); await setupDatabase(client); + arweave.wallets.jwkToAddress(JSON.parse(process.env.WALLET_JSON!)).then((address) => { + console.log(address); + PROVIDER_ID = address + //1seRanklLU_1VTGkEk7P0xAwMJfA7owA1JHW5KyZKlY + }); + setInterval(async () => { const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); console.log(`Periodic log - Current database size: ${res.rows[0].count}`); @@ -838,6 +858,7 @@ async function run(): Promise { process.on("SIGTERM", async () => { console.log("SIGTERM received. Closing database connection."); await client.end(); + await shutdown(); process.exit(0); }); } diff --git a/requester/package.json b/requester/package.json index a2e12d5..57d291a 100644 --- a/requester/package.json +++ b/requester/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "ao-process-clients": "3.7.3", + "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", "crypto": "^1.0.1", @@ -24,4 +25,4 @@ "keywords": [], "author": "", "license": "ISC" -} \ No newline at end of file +} diff --git a/terraform/ecs.tf b/terraform/ecs.tf index c57785a..53e78dd 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -62,7 +62,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([ { name = "orchestrator" - image = "randao/orchestrator:v0.3.0" + image = "randao/orchestrator:v0.3.2" environment = [ { name = "ENVIRONMENT" From 1a023c7325a84387251f7e3a961a3e67db843be4 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 14 Mar 2025 17:41:35 -0400 Subject: [PATCH 32/80] wip --- docker-compose/dev-docker-compose.yml | 8 +-- docker-compose/mass-dev-docker-compose.yml | 32 +++++------ orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 63 ++++++++++++---------- requester/package.json | 2 +- requester/src/app.ts | 32 ++++++----- 7 files changed, 78 insertions(+), 63 deletions(-) diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml index 9789769..b8a0e10 100644 --- a/docker-compose/dev-docker-compose.yml +++ b/docker-compose/dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.3 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.3 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.2.80 + image: randao/orchestrator:v0.3.3 depends_on: postgres3: condition: service_healthy @@ -133,7 +133,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.2.7 + image: randao/requester:v0.2.8 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml index 2da3ea1..f5f27cf 100644 --- a/docker-compose/mass-dev-docker-compose.yml +++ b/docker-compose/mass-dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres3: condition: service_healthy @@ -138,7 +138,7 @@ services: retries: 5 orchestrator4: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres4: condition: service_healthy @@ -177,7 +177,7 @@ services: retries: 5 orchestrator5: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres5: condition: service_healthy @@ -216,7 +216,7 @@ services: retries: 5 orchestrator6: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres6: condition: service_healthy @@ -255,7 +255,7 @@ services: retries: 5 orchestrator7: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres7: condition: service_healthy @@ -294,7 +294,7 @@ services: retries: 5 orchestrator8: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres8: condition: service_healthy @@ -333,7 +333,7 @@ services: retries: 5 orchestrator9: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres9: condition: service_healthy @@ -372,7 +372,7 @@ services: retries: 5 orchestrator10: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres10: condition: service_healthy @@ -411,7 +411,7 @@ services: retries: 5 orchestrator11: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres11: condition: service_healthy @@ -450,7 +450,7 @@ services: retries: 5 orchestrator12: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres12: condition: service_healthy @@ -489,7 +489,7 @@ services: retries: 5 orchestrator13: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres13: condition: service_healthy @@ -528,7 +528,7 @@ services: retries: 5 orchestrator14: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres14: condition: service_healthy @@ -567,7 +567,7 @@ services: retries: 5 orchestrator15: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.30 depends_on: postgres15: condition: service_healthy @@ -601,7 +601,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.2.7 + image: randao/requester:v0.3.0 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index f2fb4ba..ccfaed3 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.3.1 # You can change this value to any version you want +export VERSION=v0.3.3 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 1d32dd4..a1b1c60 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "5.2.8", + "ao-process-clients": "5.3.8", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 18c191d..99ed13c 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,7 +1,7 @@ import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; -import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RandomClientConfig} from "ao-process-clients" +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RandomClientConfig, RandomClientConfigBuilder} from "ao-process-clients" import { dbConfig } from './db_config.js'; import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; import Arweave from 'arweave'; @@ -12,17 +12,18 @@ import Arweave from 'arweave'; // processId: '' // } //const randclient: IRandomClient = RandomClient.autoConfiguration() -async function getRandomClient(): Promise{ -// let test = await getRandomClientAutoConfiguration() -// test.wallet = JSON.parse(process.env.WALLET_JSON!) - -const RANDOM_CONFIG: RandomClientConfig = { - wallet: JSON.parse(process.env.WALLET_JSON!), - tokenProcessId: '5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI', - processId: '1dnDvaDRQ7Ao6o1ohTr7NNrN5mp1CpsXFrWm3JJFEs8' -} -const randclient = new RandomClient(RANDOM_CONFIG) - return randclient +let randomClientInstance: RandomClient | null = null; + +async function getRandomClient(): Promise { + if (!randomClientInstance) { + const RANDOM_CONFIG: RandomClientConfig = await new RandomClientConfigBuilder() + .withWallet(JSON.parse(process.env.WALLET_JSON!)) + .build(); + + randomClientInstance = new RandomClient(RANDOM_CONFIG); + } + + return randomClientInstance; } // async function getStakingClient(): Promise{ @@ -34,18 +35,10 @@ const randclient = new RandomClient(RANDOM_CONFIG) const docker = new Docker(); - - - // Constants for configuration const POLLING_INTERVAL_MS = 1000; -const MINIMUM_ENTRIES = 500; -const DRYRUNTIMEOUT = 15000; // 15 seconds -//const DRYRUNRESETTIME = 300000; // 5 min -//Expected increments per second=10×0.005=0.05 -//180 times per hour -//4,320 times per day -//1,576,800 times per year +const MINIMUM_ENTRIES = 1000; +const DRYRUNTIMEOUT = 30000; // 30 seconds const MAX_OUTSTANDING_VDF_CONTAINERS = 10; const RANDOM_PER_VDF = 10; const MAX_RETRIES = 10; @@ -57,13 +50,16 @@ const ongoingContainers = new Set(); // Track container IDs of running D let PROVIDER_ID = ""; const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds +const UNCHAIN_VS_OFFCHAIN_MAX_DIF = 250; + + + let ongoingRequest = false; let spotInterruptions = 0; // Global variables to track polling status let pollingInProgress = false; let lastPollingId: string | null = null; let pulledDockerimage = false; - // Cache for network configuration let cachedNetworkConfig: NetworkConfig | null = null; @@ -385,23 +381,36 @@ async function checkAndFetchIfNeeded(client: Client) { switch (on_chain_avalible_random.availibleRandomValues) { case -1: - console.log("Provider is shutting down"); + console.log("Value is -1"); + console.log("Provider has been shut down by USER..."); + console.log("Go to the provider dashboard to turn back on"); //TODO prepare for shutdown //TODO the async causes it to ovewrite itself break; case -2: console.log("Value is -2"); + console.log("Provider has been shut down by PROCESS..."); + console.log("This is due to One of the following: "); + console.log("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); + console.log("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)" ); + console.log("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); + console.log("Go to the provider dashboard to turn back on"); break; case -3: console.log("Value is -3"); + console.log("Provider has been shut down by PROCESS..."); + console.log("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); + console.log("Go to the provider dashboard to turn back on"); break; default: console.log("Value is not -1, -2, or -3"); - console.log("Value is "+ on_chain_avalible_random.availibleRandomValues) - if (on_chain_avalible_random.availibleRandomValues != currentCount) { + console.log("Provider is up and working"); + console.log("Onchain Value is "+ on_chain_avalible_random.availibleRandomValues) + console.log("Local Value is "+ currentCount) + if (Math.abs(on_chain_avalible_random.availibleRandomValues - currentCount) > UNCHAIN_VS_OFFCHAIN_MAX_DIF) { console.log(`Updating available random values from ${on_chain_avalible_random.availibleRandomValues} to ${currentCount}`); updateAvailableValuesAsync(currentCount); - } + } } if (ongoingRequest) return; // Prevent redundant operations diff --git a/requester/package.json b/requester/package.json index 57d291a..d6ef358 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "3.7.3", + "ao-process-clients": "5.3.14", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index 673c16c..2e369cb 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -1,12 +1,15 @@ import { + ProviderStakingClient, getRandomClientAutoConfiguration, IRandomClient, RandomClient, RandomClientConfig, StakingClient, + ProviderProfileClient, + RandomClientConfigBuilder, } from "ao-process-clients"; -const RETRY_DELAY_MS = 1000; // 1 second +const RETRY_DELAY_MS = 10000; // 10 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; @@ -14,19 +17,20 @@ const CHANCE_TO_CALL_RANDOM = 1; let cachedProviders: string[] = []; let lastProviderRefresh = 0; -const RANDOM_CONFIG: RandomClientConfig = { - tokenProcessId: "5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI", - processId: "yKVS1tYE3MajUpZqEIORmW1J8HTke-6o6o6tnlkFOZQ", - wallet: JSON.parse(process.env.REQUEST_WALLET_JSON!), - environment: "mainnet", -}; - +async function getRandomClient(): Promise{ + // let test = await getRandomClientAutoConfiguration() + // test.wallet = JSON.parse(process.env.WALLET_JSON!) + + const RANDOM_CONFIG: RandomClientConfig = await new RandomClientConfigBuilder().withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)).build() + const randclient = new RandomClient(RANDOM_CONFIG) + return randclient + } let totalRandomCalled = 0; let totalTimeToFulfill = 0; let fulfilledRequests = 0; const outstandingRequests: Set = new Set(); -async function getRandomProviders(stakeclient: StakingClient): Promise<{ providers: string[], count: number }> { +async function getRandomProviders(randclient: RandomClient): Promise<{ providers: string[], count: number }> { const now = Date.now(); // If we have cached providers and they're not expired, use them @@ -49,9 +53,11 @@ async function getRandomProviders(stakeclient: StakingClient): Promise<{ provide // Create the actual provider fetch promise const fetchPromise = async () => { - const providerInfo = await stakeclient.getAllProvidersInfo(); + const providerInfo = await randclient.getAllProviderActivity(); const eligibleProviders = providerInfo + //@ts-ignore .filter(provider => provider.active === 1) + //@ts-ignore .map(provider => provider.provider_id); if (eligibleProviders.length === 0) { @@ -96,8 +102,8 @@ async function getRandomProviders(stakeclient: StakingClient): Promise<{ provide } async function main() { - const randclient = new RandomClient(RANDOM_CONFIG); - const stakeclient = new StakingClient(RANDOM_CONFIG); + const randclient = await getRandomClient() + //const stakeclient = ProviderStakingClient.autoConfiguration(); while (true) { console.log("Running") @@ -106,7 +112,7 @@ async function main() { if (Math.random() < CHANCE_TO_CALL_RANDOM) { console.log("Initiating random request..."); const callbackId = `callback-${Date.now()}`; - const { providers, count } = await getRandomProviders(stakeclient); + const { providers, count } = await getRandomProviders(randclient); console.log(`Selected ${count} providers:`, providers); await randclient.createRequest(providers, count, callbackId); totalRandomCalled++; From 84471c3d03c7d4e20a4930adb10de27ab6e13856 Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 18 Mar 2025 16:41:41 -0400 Subject: [PATCH 33/80] done --- docker-compose/.env.example | 1 - docker-compose/dev-docker-compose.yml | 6 +-- docker-compose/docker-compose.yml | 2 +- docker-compose/mass-dev-docker-compose.yml | 32 +++++++-------- orchestrator/Dockerfile | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 40 ++++++++++++++----- .../{clear_outputs.ts => clear_outputs.tzs} | 0 orchestrator/src/{stake.ts => stake.tzs} | 0 orchestrator/tsconfig.json | 12 ++++-- requester/package.json | 2 +- requester/tsconfig.json | 4 +- terraform/ecs.tf | 2 +- 14 files changed, 66 insertions(+), 41 deletions(-) rename orchestrator/src/{clear_outputs.ts => clear_outputs.tzs} (100%) rename orchestrator/src/{stake.ts => stake.tzs} (100%) diff --git a/docker-compose/.env.example b/docker-compose/.env.example index 80d1ef5..62a84ab 100644 --- a/docker-compose/.env.example +++ b/docker-compose/.env.example @@ -13,4 +13,3 @@ WALLET_JSON = '{ "dq": "test", "qi": "test" }' -PROVIDER_ID = "XUo8jZtUDBFLtp5okR12oLrqIZ4ewNlTpqnqmriihJE" diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml index b8a0e10..4833642 100644 --- a/docker-compose/dev-docker-compose.yml +++ b/docker-compose/dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.3.3 + image: randao/orchestrator:v0.3.45 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.3.3 + image: randao/orchestrator:v0.3.45 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.3.3 + image: randao/orchestrator:v0.3.45 depends_on: postgres3: condition: service_healthy diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 3d7b40c..ae00b9e 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.3.2 + image: randao/orchestrator:v0.3.45 depends_on: postgres: condition: service_healthy diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml index f5f27cf..5a689fc 100644 --- a/docker-compose/mass-dev-docker-compose.yml +++ b/docker-compose/mass-dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres3: condition: service_healthy @@ -138,7 +138,7 @@ services: retries: 5 orchestrator4: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres4: condition: service_healthy @@ -177,7 +177,7 @@ services: retries: 5 orchestrator5: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres5: condition: service_healthy @@ -216,7 +216,7 @@ services: retries: 5 orchestrator6: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres6: condition: service_healthy @@ -255,7 +255,7 @@ services: retries: 5 orchestrator7: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres7: condition: service_healthy @@ -294,7 +294,7 @@ services: retries: 5 orchestrator8: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres8: condition: service_healthy @@ -333,7 +333,7 @@ services: retries: 5 orchestrator9: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres9: condition: service_healthy @@ -372,7 +372,7 @@ services: retries: 5 orchestrator10: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres10: condition: service_healthy @@ -411,7 +411,7 @@ services: retries: 5 orchestrator11: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres11: condition: service_healthy @@ -450,7 +450,7 @@ services: retries: 5 orchestrator12: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres12: condition: service_healthy @@ -489,7 +489,7 @@ services: retries: 5 orchestrator13: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres13: condition: service_healthy @@ -528,7 +528,7 @@ services: retries: 5 orchestrator14: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres14: condition: service_healthy @@ -567,7 +567,7 @@ services: retries: 5 orchestrator15: - image: randao/orchestrator:v0.3.30 + image: randao/orchestrator:v0.3.45 depends_on: postgres15: condition: service_healthy @@ -601,7 +601,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.3.0 + image: randao/requester:v0.3.1 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/orchestrator/Dockerfile b/orchestrator/Dockerfile index 0ce3931..3bdc53a 100644 --- a/orchestrator/Dockerfile +++ b/orchestrator/Dockerfile @@ -1,5 +1,5 @@ # Use the official lightweight Node.js image -FROM node:20-bullseye-slim +FROM node:22-bullseye-slim # Create a working directory WORKDIR /usr/src/app diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index ccfaed3..83c8940 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.3.3 # You can change this value to any version you want +export VERSION=v0.3.45 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index a1b1c60..e8f08fa 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "5.3.8", + "ao-process-clients": "5.3.21", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 99ed13c..e06bbc4 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,7 +1,7 @@ import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; -import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RandomClientConfig, RandomClientConfigBuilder} from "ao-process-clients" +import { BaseClientConfig, BaseClientConfigBuilder, GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RandomClientConfig, RandomClientConfigBuilder} from "ao-process-clients" import { dbConfig } from './db_config.js'; import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; import Arweave from 'arweave'; @@ -26,6 +26,22 @@ async function getRandomClient(): Promise { return randomClientInstance; } +// async function getRandomClient(): Promise { +// if (!randomClientInstance) { +// const RANDOM_CONFIG: BaseClientConfigBuilder = await new BaseClientConfigBuilder() +// .withWallet(JSON.parse(process.env.WALLET_JSON!)) +// .withAOConfig({ +// CU_URL: "https://cu.randao.net", +// MODE: 'legacy' +// }) +// .build(); + +// randomClientInstance = new RandomClient(RANDOM_CONFIG); +// } + +// return randomClientInstance; +// } + // async function getStakingClient(): Promise{ // let test = await getProviderStakingClientAutoConfiguration() // test.wallet = JSON.parse(process.env.WALLET_JSON!) @@ -311,7 +327,8 @@ function addHexPrefix(value: string): string { function updateAvailableValuesAsync(currentCount: number) { return (async () => { try { - (await getRandomClient()).updateProviderAvailableValues(currentCount); + const randomClient = await getRandomClient(); + await randomClient.updateProviderAvailableValues(currentCount); console.log(`Updated provider values to ${currentCount}`); } catch (error) { console.error("Failed to update provider values:", error); @@ -319,18 +336,19 @@ function updateAvailableValuesAsync(currentCount: number) { })(); } + async function shutdown() { - return (async () => { - try { - let message = await (await getRandomClient()).updateProviderAvailableValues(0); - console.log(message) - console.log(`Updated provider values to ${0}`); - } catch (error) { - console.error("Failed to update provider values:", error); - } - })(); + try { + const randomClient = await getRandomClient(); + let message = await randomClient.updateProviderAvailableValues(0); + console.log(message); + console.log(`Updated provider values to 0`); + } catch (error) { + console.error("Failed to update provider values:", error); + } } + async function getMoreRandom(currentCount: number) { const entriesNeeded = MINIMUM_ENTRIES - currentCount; //TODO this math looked wrong in https://discord.com/channels/1209645894039896074/1333434937537204336/1336000074765045853 diff --git a/orchestrator/src/clear_outputs.ts b/orchestrator/src/clear_outputs.tzs similarity index 100% rename from orchestrator/src/clear_outputs.ts rename to orchestrator/src/clear_outputs.tzs diff --git a/orchestrator/src/stake.ts b/orchestrator/src/stake.tzs similarity index 100% rename from orchestrator/src/stake.ts rename to orchestrator/src/stake.tzs diff --git a/orchestrator/tsconfig.json b/orchestrator/tsconfig.json index bf50834..786e06e 100644 --- a/orchestrator/tsconfig.json +++ b/orchestrator/tsconfig.json @@ -8,11 +8,17 @@ "esModuleInterop": true, "moduleResolution": "node", "resolveJsonModule": true, - "declaration": true + "declaration": true, + "skipLibCheck": true, // ✅ Added to skip library type checking + "typeRoots": ["./node_modules/@types"] // ✅ Added to force correct type resolution }, "include": [ - "src/**/*.ts" -, "src/db_config.ts", "src/clear_all_output_requests.ts", "src/reset_db.mjs" ], + "src/**/*.ts", + "src/db_config.ts", + "src/clear_all_output_requests.ts", + "src/reset_db.mjs", + "src/clear_outputs.tzs" + ], "exclude": [ "node_modules" ] diff --git a/requester/package.json b/requester/package.json index d6ef358..59e9853 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "5.3.14", + "ao-process-clients": "5.3.21", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/tsconfig.json b/requester/tsconfig.json index 493e143..50dc9ee 100644 --- a/requester/tsconfig.json +++ b/requester/tsconfig.json @@ -5,7 +5,9 @@ "module": "commonjs", "target": "es6", "strict": true, - "esModuleInterop": true + "esModuleInterop": true, + "skipLibCheck": true, // ✅ Added to skip library type checking + "typeRoots": ["./node_modules/@types"] // ✅ Added to force correct type resolution }, "include": [ "src/**/*.ts" diff --git a/terraform/ecs.tf b/terraform/ecs.tf index 53e78dd..ccd5cee 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -62,7 +62,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([ { name = "orchestrator" - image = "randao/orchestrator:v0.3.2" + image = "randao/orchestrator:v0.3.45" environment = [ { name = "ENVIRONMENT" From 062d82d629ea1c7c827f3e4bbc2a9ed4fac285d2 Mon Sep 17 00:00:00 2001 From: K <111819113+KennySwayzee93@users.noreply.github.com> Date: Thu, 20 Mar 2025 21:19:11 -0400 Subject: [PATCH 34/80] time lock puzzles are through database - folder still needs cleanup of old method --- verifiable-delay-function/.pylintrc | 42 ++++++ .../main_time_lock_puzzle.py | 124 ++++++++++++++++++ .../src/converters/__init__.py | 7 + .../src/converters/rsa_converter.py | 25 ++++ .../converters/time_lock_puzzle_converter.py | 24 ++++ .../src/database/DatabaseService.py | 17 +++ .../src/database/entity/RSAEntity.py | 40 ++++++ .../database/entity/TimeLockPuzzleEntity.py | 36 +++++ .../src/database/entity/__init__.py | 7 + verifiable-delay-function/src/mpc/MPC.py | 35 +++++ verifiable-delay-function/src/mpc/__init__.py | 7 + .../src/mpc/abstract/IMPC.py | 95 ++++++++++++++ .../src/mpc/abstract/__init__.py | 5 + verifiable-delay-function/src/mpc/types.py | 11 ++ .../src/primes/Primes.py | 18 +++ .../src/primes/__init__.py | 6 + .../src/primes/abstract/IPrimes.py | 18 +++ .../src/primes/abstract/__init__.py | 5 + .../src/random/Random.py | 13 ++ .../src/random/__init__.py | 6 + .../src/random/abstract/IRandom.py | 18 +++ .../src/random/abstract/__init__.py | 5 + verifiable-delay-function/src/rsa/RSA.py | 52 ++++++++ verifiable-delay-function/src/rsa/__init__.py | 6 + .../src/rsa/abstract/IRSA.py | 46 +++++++ .../src/rsa/abstract/__init__.py | 0 .../EfficientTimeLockPuzzleSolver.py | 45 +++++++ .../SequentialTimeLockPuzzleSolver.py | 33 +++++ .../src/time_lock_puzzle/TimeLockPuzzle.py | 27 ++++ .../time_lock_puzzle/TimeLockPuzzleBuilder.py | 30 +++++ .../time_lock_puzzle/TimeLockPuzzleFactory.py | 78 +++++++++++ .../src/time_lock_puzzle/__init__.py | 25 ++++ .../IEfficientTimeLockPuzzleSolver.py | 36 +++++ .../ISequentialTimeLockPuzzleSolver.py | 19 +++ .../abstract/ITimeLockPuzzle.py | 30 +++++ .../abstract/ITimeLockPuzzleBuilder.py | 49 +++++++ .../abstract/ITimeLockPuzzleFactory.py | 34 +++++ .../src/time_lock_puzzle/abstract/__init__.py | 0 .../src/time_lock_puzzle/constants.py | 5 + .../verifiable_delay_function.py | 36 +++-- .../test_time_lock_puzzle_converter.py | 50 ------- .../entity/test_time_lock_puzzle_entity.py | 58 -------- ...st_time_lock_puzzle_basic_functionality.py | 45 ------- .../test_time_lock_puzzle_sample.py | 120 ----------------- 44 files changed, 1102 insertions(+), 286 deletions(-) create mode 100644 verifiable-delay-function/.pylintrc create mode 100644 verifiable-delay-function/main_time_lock_puzzle.py create mode 100644 verifiable-delay-function/src/converters/rsa_converter.py create mode 100644 verifiable-delay-function/src/converters/time_lock_puzzle_converter.py create mode 100644 verifiable-delay-function/src/database/DatabaseService.py create mode 100644 verifiable-delay-function/src/database/entity/RSAEntity.py create mode 100644 verifiable-delay-function/src/database/entity/TimeLockPuzzleEntity.py create mode 100644 verifiable-delay-function/src/mpc/MPC.py create mode 100644 verifiable-delay-function/src/mpc/__init__.py create mode 100644 verifiable-delay-function/src/mpc/abstract/IMPC.py create mode 100644 verifiable-delay-function/src/mpc/abstract/__init__.py create mode 100644 verifiable-delay-function/src/mpc/types.py create mode 100644 verifiable-delay-function/src/primes/Primes.py create mode 100644 verifiable-delay-function/src/primes/__init__.py create mode 100644 verifiable-delay-function/src/primes/abstract/IPrimes.py create mode 100644 verifiable-delay-function/src/primes/abstract/__init__.py create mode 100644 verifiable-delay-function/src/random/Random.py create mode 100644 verifiable-delay-function/src/random/__init__.py create mode 100644 verifiable-delay-function/src/random/abstract/IRandom.py create mode 100644 verifiable-delay-function/src/random/abstract/__init__.py create mode 100644 verifiable-delay-function/src/rsa/RSA.py create mode 100644 verifiable-delay-function/src/rsa/__init__.py create mode 100644 verifiable-delay-function/src/rsa/abstract/IRSA.py create mode 100644 verifiable-delay-function/src/rsa/abstract/__init__.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzle.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleBuilder.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleFactory.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/__init__.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/abstract/__init__.py create mode 100644 verifiable-delay-function/src/time_lock_puzzle/constants.py delete mode 100644 verifiable-delay-function/tests/converters/test_time_lock_puzzle_converter.py delete mode 100644 verifiable-delay-function/tests/database/entity/test_time_lock_puzzle_entity.py delete mode 100644 verifiable-delay-function/tests/time_lock_puzzle/test_time_lock_puzzle_basic_functionality.py delete mode 100644 verifiable-delay-function/tests/time_lock_puzzle/test_time_lock_puzzle_sample.py diff --git a/verifiable-delay-function/.pylintrc b/verifiable-delay-function/.pylintrc new file mode 100644 index 0000000..4b2ad19 --- /dev/null +++ b/verifiable-delay-function/.pylintrc @@ -0,0 +1,42 @@ +[MASTER] +# Add the gmpy2 module to the list of known third party modules +extension-pkg-whitelist=gmpy2 + +# Python code to execute, usually for sys.path manipulation such as pygtk.require() +init-hook='import sys; sys.path.append(".")' + +[MESSAGES CONTROL] +# Disable specific warnings +disable=C0111, # Missing docstring + C0103, # Invalid name + C0303, # Trailing whitespace + E1101, # No member (since gmpy2 uses dynamic members) + R0903, # Too Few public methods + +[TYPECHECK] +# List of module names for which member attributes should not be checked +ignored-modules=gmpy2 + +# List of classes names for which member attributes should not be checked +ignored-classes=gmpy2.mpz,gmpy2.random_state + +[FORMAT] +# Maximum number of characters on a single line +max-line-length=100 + +# Number of spaces of indent required inside a hanging or continued line +indent-after-paren=4 + +[BASIC] +# Regular expression which should only match function or class names +function-rgx=[a-z_][a-z0-9_]{2,50}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +[REPORTS] +# Set the output format. Available formats are text, parseable, colorized +output-format=colorized + +# Include a brief explanation of each error when errors are displayed +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} diff --git a/verifiable-delay-function/main_time_lock_puzzle.py b/verifiable-delay-function/main_time_lock_puzzle.py new file mode 100644 index 0000000..dc134a7 --- /dev/null +++ b/verifiable-delay-function/main_time_lock_puzzle.py @@ -0,0 +1,124 @@ +"""Main script for generating and persisting time lock puzzles.""" + +import argparse +import time +from typing import List, Tuple + +from src.converters.rsa_converter import RSAConverter +from src.converters.time_lock_puzzle_converter import TimeLockPuzzleConverter +from src.database.DatabaseService import DatabaseService +from src.database.entity.RSAEntity import RSAEntity +from src.database.entity.TimeLockPuzzleEntity import TimeLockPuzzleEntity +from src.mpc import MPC +from src.mpc.types import MPZ +from src.rsa.RSA import RSA +from src.time_lock_puzzle.TimeLockPuzzle import TimeLockPuzzle +from src.time_lock_puzzle.TimeLockPuzzleFactory import TimeLockPuzzleFactory + +# Constants +BIT_SIZE = 2048 # Size for RSA parameters +TIMING_PARAMETER = MPC.mpz(3_000_000) # Number of squarings required + + +class TimeLockPuzzleService: + """Service class for managing time lock puzzle operations.""" + + def __init__(self, bit_size: int, timing_parameter: MPC.mpz): + """ + Initialize the service. + + Args: + bit_size: Size for RSA parameters + timing_parameter: Number of squarings required + """ + self.factory = TimeLockPuzzleFactory(bit_size, timing_parameter) + self.rsa_converter = RSAConverter() + self.puzzle_converter = TimeLockPuzzleConverter() + + def generate_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + """ + Generate multiple time lock puzzles. + + Args: + amount: Number of puzzles to generate + + Returns: + List of (puzzle, rsa) tuples + """ + print(f"Generating {amount} puzzles...") + start_time = time.time() + puzzles = self.factory.create_puzzles(amount) + + total_time = time.time() - start_time + print(f"Puzzle generation took {total_time:.2f} seconds") + return puzzles + + def convert_to_entities( + self, puzzles: List[Tuple[TimeLockPuzzle, RSA, MPZ]] + ) -> List[TimeLockPuzzleEntity | RSAEntity]: + """ + Convert puzzles and RSAs to database entities. + + Args: + puzzles: List of (puzzle, rsa) tuples + + Returns: + List of entities to save + """ + print("\nConverting to entities...") + start_time = time.time() + entities = [] + for puzzle, rsa, _ in puzzles: + rsa_entity = self.rsa_converter.to_entity(rsa) + puzzle_entity = self.puzzle_converter.to_entity(puzzle) + entities.extend([rsa_entity, puzzle_entity]) + total_time = time.time() - start_time + print(f"Entity conversion took {total_time:.2f} seconds") + return entities + + def save_entities(self, entities: List[TimeLockPuzzleEntity | RSAEntity]) -> None: + """ + Save entities to database. + + Args: + entities: List of entities to save + """ + print("\nSaving to database...") + start_time = time.time() + DatabaseService.save_many(entities) + total_time = time.time() - start_time + print(f"Database save took {total_time:.2f} seconds") + + +def parse_args() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Generate and save time lock puzzles.") + parser.add_argument( + "count", + type=int, + help="Number of time lock puzzles to generate", + ) + return parser.parse_args() + + +def main() -> None: + """Generate time lock puzzles and save them to the database.""" + args = parse_args() + + # Initialize service + service = TimeLockPuzzleService(BIT_SIZE, TIMING_PARAMETER) + + # Generate puzzles + puzzles = service.generate_puzzles(args.count) + + # Convert to entities + entities = service.convert_to_entities(puzzles) + + # Save to database + service.save_entities(entities) + + print("\nDone!") + + +if __name__ == "__main__": + main() diff --git a/verifiable-delay-function/src/converters/__init__.py b/verifiable-delay-function/src/converters/__init__.py index e69de29..6db91fe 100644 --- a/verifiable-delay-function/src/converters/__init__.py +++ b/verifiable-delay-function/src/converters/__init__.py @@ -0,0 +1,7 @@ +"""Converters for database entities.""" + +from .verifiable_delay_function_converter import * +from .time_lock_puzzle_converter import TimeLockPuzzleConverter +from .rsa_converter import RSAConverter + +__all__ = ["TimeLockPuzzleConverter", "RSAConverter"] diff --git a/verifiable-delay-function/src/converters/rsa_converter.py b/verifiable-delay-function/src/converters/rsa_converter.py new file mode 100644 index 0000000..8c5c1c3 --- /dev/null +++ b/verifiable-delay-function/src/converters/rsa_converter.py @@ -0,0 +1,25 @@ +"""Converter for RSA objects.""" + +from src.rsa.RSA import RSA +from src.database.entity.RSAEntity import RSAEntity + + +class RSAConverter: + """Converter for storing RSA parameters in the database.""" + + @staticmethod + def to_entity(rsa: RSA) -> RSAEntity: + """Convert an RSA instance to an RSAEntity. + + Args: + rsa (RSA): The RSA instance to convert + + Returns: + RSAEntity: The database entity + """ + return RSAEntity( + hex(rsa.get_p()), + hex(rsa.get_q()), + hex(rsa.get_N()), + hex(rsa.get_phi()), + ) diff --git a/verifiable-delay-function/src/converters/time_lock_puzzle_converter.py b/verifiable-delay-function/src/converters/time_lock_puzzle_converter.py new file mode 100644 index 0000000..6abedef --- /dev/null +++ b/verifiable-delay-function/src/converters/time_lock_puzzle_converter.py @@ -0,0 +1,24 @@ +"""Converter for time lock puzzle objects.""" + +from src.time_lock_puzzle import TimeLockPuzzle +from src.database.entity.TimeLockPuzzleEntity import TimeLockPuzzleEntity + + +class TimeLockPuzzleConverter: + """Converter between TimeLockPuzzle and TimeLockPuzzleEntity.""" + + @staticmethod + def to_entity(puzzle: TimeLockPuzzle) -> TimeLockPuzzleEntity: + """Convert a TimeLockPuzzle to a TimeLockPuzzleEntity. + + Args: + puzzle (TimeLockPuzzle): The puzzle to convert + + Returns: + TimeLockPuzzleEntity: The database entity + """ + return TimeLockPuzzleEntity( + x_hex=hex(puzzle.get_x()), + t=str(puzzle.get_t()), + N_hex=hex(puzzle.get_N()), + ) diff --git a/verifiable-delay-function/src/database/DatabaseService.py b/verifiable-delay-function/src/database/DatabaseService.py new file mode 100644 index 0000000..0df181e --- /dev/null +++ b/verifiable-delay-function/src/database/DatabaseService.py @@ -0,0 +1,17 @@ +from typing import List +from .mixins.saveable import Saveable + + +class DatabaseService: + """Service class for database operations.""" + + @staticmethod + def save_many(instances: List[Saveable]) -> None: + """ + Save multiple instances to the database. + + Args: + instances: List of Saveable instances to save + """ + for instance in instances: + instance.save() diff --git a/verifiable-delay-function/src/database/entity/RSAEntity.py b/verifiable-delay-function/src/database/entity/RSAEntity.py new file mode 100644 index 0000000..ffdf98f --- /dev/null +++ b/verifiable-delay-function/src/database/entity/RSAEntity.py @@ -0,0 +1,40 @@ +import uuid +from sqlalchemy import Column, String +from sqlalchemy.ext.declarative import declarative_base + +from src.database.mixins.saveable import Saveable +from src.database.database import get_orm_base + +# Define the Base class for ORM models +Base = get_orm_base() + + +class RSAEntity(Base, Saveable): + """Database entity for storing RSA parameters.""" + + __tablename__ = "rsa_parameters" + + id = Column( + String, primary_key=True, default=lambda: str(uuid.uuid4()) + ) # Unique generated string ID + p = Column(String, nullable=False) # Store hex string of prime p + q = Column(String, nullable=False) # Store hex string of prime q + N = Column(String, nullable=False) # Store hex string of modulus N + phi = Column(String, nullable=False) # Store hex string of Euler's totient + + def __repr__(self): + return f"" + + def __init__(self, p_hex: str, q_hex: str, N_hex: str, phi_hex: str): + """Initialize an RSA entity. + + Args: + p_hex (str): Hex string of prime p + q_hex (str): Hex string of prime q + N_hex (str): Hex string of modulus N + phi_hex (str): Hex string of Euler's totient + """ + self.p = p_hex + self.q = q_hex + self.N = N_hex + self.phi = phi_hex diff --git a/verifiable-delay-function/src/database/entity/TimeLockPuzzleEntity.py b/verifiable-delay-function/src/database/entity/TimeLockPuzzleEntity.py new file mode 100644 index 0000000..bb3b4ca --- /dev/null +++ b/verifiable-delay-function/src/database/entity/TimeLockPuzzleEntity.py @@ -0,0 +1,36 @@ +import uuid +from sqlalchemy import Column, String + +from src.database.mixins.saveable import Saveable +from src.database.database import get_orm_base + +# Define the Base class for ORM models +Base = get_orm_base() + + +class TimeLockPuzzleEntity(Base, Saveable): + """Database entity for storing time lock puzzles.""" + + __tablename__ = "time_lock_puzzles" + + id = Column( + String, primary_key=True, default=lambda: str(uuid.uuid4()) + ) # Unique generated string ID + x = Column(String, nullable=False) # Store hex string of input value x + t = Column(String, nullable=False) # Store base 10 string of time parameter t + N = Column(String, nullable=False) # Store hex string of modulus N + + def __repr__(self): + return f"" + + def __init__(self, x_hex: str, t: str, N_hex: str): + """Initialize a time lock puzzle entity. + + Args: + x_hex (str): Hex string of input value x + t (str): Base 10 string of time parameter t + N_hex (str): Hex string of modulus N + """ + self.x = x_hex + self.t = t + self.N = N_hex diff --git a/verifiable-delay-function/src/database/entity/__init__.py b/verifiable-delay-function/src/database/entity/__init__.py index e69de29..6eb1c9b 100644 --- a/verifiable-delay-function/src/database/entity/__init__.py +++ b/verifiable-delay-function/src/database/entity/__init__.py @@ -0,0 +1,7 @@ +"""Database entity models.""" + +from .verifiable_delay_function_entity import VerifiableDelayFunctionEntity +from .TimeLockPuzzleEntity import TimeLockPuzzleEntity +from .RSAEntity import RSAEntity + +__all__ = ["VerifiableDelayFunctionEntity", "TimeLockPuzzleEntity", "RSAEntity"] diff --git a/verifiable-delay-function/src/mpc/MPC.py b/verifiable-delay-function/src/mpc/MPC.py new file mode 100644 index 0000000..169c273 --- /dev/null +++ b/verifiable-delay-function/src/mpc/MPC.py @@ -0,0 +1,35 @@ +import gmpy2 +from .abstract.IMPC import IMPC +from .types import MPZ, RandomState + + +class MPC(IMPC): + """Implementation of multi-precision computing operations.""" + + @staticmethod + def mpz(value: int) -> MPZ: + return gmpy2.mpz(value) + + @staticmethod + def random_state(seed: int) -> RandomState: + return gmpy2.random_state(seed) + + @staticmethod + def mpz_urandomb(state: RandomState, bit_count: int) -> MPZ: + return gmpy2.mpz_urandomb(state, bit_count) + + @staticmethod + def next_prime(value: MPZ) -> MPZ: + return gmpy2.next_prime(value) + + @staticmethod + def powmod(base: MPZ, exp: MPZ, mod: MPZ) -> MPZ: + return gmpy2.powmod(base, exp, mod) + + @staticmethod + def pow(base: MPZ, exp: MPZ) -> MPZ: + return base**exp + + @staticmethod + def mod(value: MPZ, modulus: MPZ) -> MPZ: + return value % modulus # gmpy2 supports % operator for mpz values diff --git a/verifiable-delay-function/src/mpc/__init__.py b/verifiable-delay-function/src/mpc/__init__.py new file mode 100644 index 0000000..ab216aa --- /dev/null +++ b/verifiable-delay-function/src/mpc/__init__.py @@ -0,0 +1,7 @@ +"""Multi-precision computing module.""" + +from .MPC import MPC +from .abstract.IMPC import IMPC +from .types import MPZ, RandomState, T + +__all__ = ["MPC", "IMPC", "MPZ", "RandomState", "T"] diff --git a/verifiable-delay-function/src/mpc/abstract/IMPC.py b/verifiable-delay-function/src/mpc/abstract/IMPC.py new file mode 100644 index 0000000..4912662 --- /dev/null +++ b/verifiable-delay-function/src/mpc/abstract/IMPC.py @@ -0,0 +1,95 @@ +from abc import ABC, abstractmethod +from ..types import MPZ, RandomState + + +class IMPC(ABC): + """Abstract base class defining the interface for multi-precision computing operations.""" + + @staticmethod + @abstractmethod + def mpz(value: int) -> MPZ: + """Convert a Python integer to an mpz. + + Args: + value (int): Integer value to convert + + Returns: + mpz: Multi-precision integer + """ + + @staticmethod + @abstractmethod + def random_state(seed: int) -> RandomState: + """Create a random state from a seed. + + Args: + seed (int): Seed value for random state + + Returns: + mpz: Random state object + """ + + @staticmethod + @abstractmethod + def mpz_urandomb(state: RandomState, bit_count: int) -> MPZ: + """Generate a random integer with specified number of bits. + + Args: + state (mpz): Random state to use + bit_count (int): Number of bits in result + + Returns: + mpz: Random integer + """ + + @staticmethod + @abstractmethod + def next_prime(value: MPZ) -> MPZ: + """Find the next prime number after the given value. + + Args: + value (mpz): Starting value + + Returns: + mpz: Next prime number + """ + + @staticmethod + @abstractmethod + def powmod(base: MPZ, exp: MPZ, mod: MPZ) -> MPZ: + """Compute (base ** exp) % mod efficiently. + + Args: + base (mpz): Base value + exp (mpz): Exponent value + mod (mpz): Modulus value + + Returns: + mpz: Result of modular exponentiation + """ + + @staticmethod + @abstractmethod + def pow(base: MPZ, exp: MPZ) -> MPZ: + """Compute base ** exp. + + Args: + base (mpz): Base value + exp (mpz): Exponent value + + Returns: + mpz: Result of exponentiation + """ + + @staticmethod + @abstractmethod + def mod(value: MPZ, modulus: MPZ) -> MPZ: + """Compute value % modulus. + + Args: + value (mpz): Value to reduce + modulus (mpz): Modulus to reduce by + + Returns: + mpz: Result of modular reduction + """ diff --git a/verifiable-delay-function/src/mpc/abstract/__init__.py b/verifiable-delay-function/src/mpc/abstract/__init__.py new file mode 100644 index 0000000..557827b --- /dev/null +++ b/verifiable-delay-function/src/mpc/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for multi-precision computing operations.""" + +from .IMPC import IMPC + +__all__ = ["IMPC"] diff --git a/verifiable-delay-function/src/mpc/types.py b/verifiable-delay-function/src/mpc/types.py new file mode 100644 index 0000000..b9497fd --- /dev/null +++ b/verifiable-delay-function/src/mpc/types.py @@ -0,0 +1,11 @@ +"""Type definitions for multi-precision computing operations.""" + +from typing import TypeVar, NewType +from gmpy2 import mpz as _mpz, random_state as _random_state + +# Define base types from gmpy2 +MPZ = NewType("MPZ", _mpz) +RandomState = NewType("RandomState", _random_state) + +# Generic type variable for numeric operations +T = TypeVar("T", MPZ, int) diff --git a/verifiable-delay-function/src/primes/Primes.py b/verifiable-delay-function/src/primes/Primes.py new file mode 100644 index 0000000..6615545 --- /dev/null +++ b/verifiable-delay-function/src/primes/Primes.py @@ -0,0 +1,18 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from ..random import Random +from .abstract.IPrimes import IPrimes + + +class Primes(IPrimes): + """Implementation of prime number generation.""" + + @staticmethod + def get_prime(bit_size: int) -> MPZ: + # Get random state for generating random numbers + rand = Random.get_random(bit_size) + + random_num = MPC.mpz_urandomb(rand, bit_size) + + # Get next prime after the random number + return MPC.next_prime(random_num) diff --git a/verifiable-delay-function/src/primes/__init__.py b/verifiable-delay-function/src/primes/__init__.py new file mode 100644 index 0000000..52cdf79 --- /dev/null +++ b/verifiable-delay-function/src/primes/__init__.py @@ -0,0 +1,6 @@ +"""Prime number generation module.""" + +from .Primes import Primes +from .abstract.IPrimes import IPrimes + +__all__ = ["Primes", "IPrimes"] diff --git a/verifiable-delay-function/src/primes/abstract/IPrimes.py b/verifiable-delay-function/src/primes/abstract/IPrimes.py new file mode 100644 index 0000000..2d49682 --- /dev/null +++ b/verifiable-delay-function/src/primes/abstract/IPrimes.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class IPrimes(ABC): + """Abstract base class defining the interface for prime number generation.""" + + @staticmethod + @abstractmethod + def get_prime(bit_size: int) -> MPZ: + """Get a random prime number. + + Args: + bit_size (int): Number of bits for the prime number. + + Returns: + MPZ: A random prime number + """ diff --git a/verifiable-delay-function/src/primes/abstract/__init__.py b/verifiable-delay-function/src/primes/abstract/__init__.py new file mode 100644 index 0000000..0ee899c --- /dev/null +++ b/verifiable-delay-function/src/primes/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for prime number generation.""" + +from .IPrimes import IPrimes + +__all__ = ["IPrimes"] diff --git a/verifiable-delay-function/src/random/Random.py b/verifiable-delay-function/src/random/Random.py new file mode 100644 index 0000000..d5e1458 --- /dev/null +++ b/verifiable-delay-function/src/random/Random.py @@ -0,0 +1,13 @@ +import secrets +from ..mpc import MPC +from ..mpc.types import RandomState +from .abstract.IRandom import IRandom + + +class Random(IRandom): + """Implementation of secure random number generation.""" + + @staticmethod + def get_random(bit_size: int) -> RandomState: + secure_seed = secrets.randbits(bit_size) + return MPC.random_state(secure_seed) diff --git a/verifiable-delay-function/src/random/__init__.py b/verifiable-delay-function/src/random/__init__.py new file mode 100644 index 0000000..3c8b236 --- /dev/null +++ b/verifiable-delay-function/src/random/__init__.py @@ -0,0 +1,6 @@ +"""Random number generation module.""" + +from .Random import Random +from .abstract.IRandom import IRandom + +__all__ = ["Random", "IRandom"] diff --git a/verifiable-delay-function/src/random/abstract/IRandom.py b/verifiable-delay-function/src/random/abstract/IRandom.py new file mode 100644 index 0000000..2e6a0ce --- /dev/null +++ b/verifiable-delay-function/src/random/abstract/IRandom.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from ...mpc.types import RandomState + + +class IRandom(ABC): + """Abstract base class defining the interface for random number generation.""" + + @staticmethod + @abstractmethod + def get_random(bit_size: int) -> RandomState: + """Get a random state initialized with a secure seed. + + Args: + bit_size (int): Number of bits for the secure seed. + + Returns: + RandomState: A random state initialized with a secure seed + """ diff --git a/verifiable-delay-function/src/random/abstract/__init__.py b/verifiable-delay-function/src/random/abstract/__init__.py new file mode 100644 index 0000000..b22a9fd --- /dev/null +++ b/verifiable-delay-function/src/random/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for random number generation.""" + +from .IRandom import IRandom + +__all__ = ["IRandom"] diff --git a/verifiable-delay-function/src/rsa/RSA.py b/verifiable-delay-function/src/rsa/RSA.py new file mode 100644 index 0000000..45b5bae --- /dev/null +++ b/verifiable-delay-function/src/rsa/RSA.py @@ -0,0 +1,52 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from .abstract.IRSA import IRSA +from ..primes import Primes + + +class RSA(IRSA): + """Implementation of RSA cryptosystem.""" + + def __init__(self, bit_size: int) -> None: + """Initialize RSA by generating two random prime numbers. + + Args: + bit_size (int): Number of bits for RSA modulus. + Each prime will be bit_size/2 bits. + """ + # Generate two random prime numbers + prime_size = ( + bit_size // 2 - 1 + ) # Each prime is half the size TODO is this needed anymore with gmpc on chain? + self._p = Primes.get_prime(prime_size) + self._q = Primes.get_prime(prime_size) + + # Calculate modulus N and Euler's totient + self._N = self._calculate_N() + self._phi = self._calculate_phi() + + def get_p(self) -> MPZ: + return self._p + + def get_q(self) -> MPZ: + return self._q + + def get_N(self) -> MPZ: + return self._N + + def get_phi(self) -> MPZ: + return self._phi + + def get_eulers_totient(self) -> MPZ: + return self.get_phi() + + # Private methods + # -------------- + + def _calculate_N(self) -> MPZ: + """Calculate the RSA modulus N = p * q.""" + return MPC.mpz(self._p * self._q) + + def _calculate_phi(self) -> MPZ: + """Calculate Euler's totient φ(N) = (p-1)(q-1).""" + return MPC.mpz((self._p - 1) * (self._q - 1)) diff --git a/verifiable-delay-function/src/rsa/__init__.py b/verifiable-delay-function/src/rsa/__init__.py new file mode 100644 index 0000000..4ac513e --- /dev/null +++ b/verifiable-delay-function/src/rsa/__init__.py @@ -0,0 +1,6 @@ +"""RSA cryptosystem module.""" + +from .RSA import RSA +from .abstract.IRSA import IRSA + +__all__ = ["RSA", "IRSA"] diff --git a/verifiable-delay-function/src/rsa/abstract/IRSA.py b/verifiable-delay-function/src/rsa/abstract/IRSA.py new file mode 100644 index 0000000..9897017 --- /dev/null +++ b/verifiable-delay-function/src/rsa/abstract/IRSA.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class IRSA(ABC): + """Abstract base class defining the interface for RSA cryptosystem implementation.""" + + @abstractmethod + def get_p(self) -> MPZ: + """Get the first prime factor p. + + Returns: + MPZ: The prime number p + """ + + @abstractmethod + def get_q(self) -> MPZ: + """Get the second prime factor q. + + Returns: + MPZ: The prime number q + """ + + @abstractmethod + def get_N(self) -> MPZ: + """Get the modulus N = p * q. + + Returns: + MPZ: The modulus N + """ + + @abstractmethod + def get_phi(self) -> MPZ: + """Get Euler's totient φ(N) = (p-1)(q-1). + + Returns: + MPZ: The value of Euler's totient function + """ + + @abstractmethod + def get_eulers_totient(self) -> MPZ: + """Alias for get_phi(). + + Returns: + MPZ: The value of Euler's totient function + """ diff --git a/verifiable-delay-function/src/rsa/abstract/__init__.py b/verifiable-delay-function/src/rsa/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/verifiable-delay-function/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py b/verifiable-delay-function/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py new file mode 100644 index 0000000..996909f --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py @@ -0,0 +1,45 @@ +from multiprocessing import Pool +from typing import List, Tuple + +from ..mpc import MPC +from ..mpc.types import MPZ +from ..rsa.RSA import RSA +from .abstract.IEfficientTimeLockPuzzleSolver import IEfficientTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .constants import TWO + + +class EfficientTimeLockPuzzleSolver(IEfficientTimeLockPuzzleSolver): + """Implementation of efficient time lock puzzle solver using RSA private parameters.""" + + @staticmethod + def solve(rsa: RSA, puzzle: ITimeLockPuzzle) -> MPZ: + # Calculate y = x^(2^t) mod N efficiently using phi + # Calculate 2^t + exp = MPC.pow(TWO, puzzle.get_t()) # 2^t + phi = rsa.get_phi() + d = MPC.mod(exp, phi) # Reduce exponent modulo phi + return MPC.powmod(puzzle.get_x(), d, puzzle.get_N()) + + @staticmethod + def solve_many(puzzles: List[Tuple[RSA, ITimeLockPuzzle]]) -> List[MPZ]: + """ + Solve multiple time lock puzzles in parallel using multiprocessing. + + Args: + puzzles: List of tuples containing (RSA, puzzle) pairs to solve + + Returns: + List of solutions in the same order as input puzzles + """ + with Pool() as pool: + return pool.map(EfficientTimeLockPuzzleSolver._solve_single, puzzles) + + # Private Methods + # -------------- + + @staticmethod + def _solve_single(args: Tuple[RSA, ITimeLockPuzzle]) -> MPZ: + """Helper method to solve a single puzzle for multiprocessing.""" + rsa, puzzle = args + return EfficientTimeLockPuzzleSolver.solve(rsa, puzzle) diff --git a/verifiable-delay-function/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py b/verifiable-delay-function/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py new file mode 100644 index 0000000..0144200 --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py @@ -0,0 +1,33 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from .abstract.ISequentialTimeLockPuzzleSolver import ISequentialTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .constants import TWO + + +class SequentialTimeLockPuzzleSolver(ISequentialTimeLockPuzzleSolver): + """Implementation of sequential time lock puzzle solver.""" + + @staticmethod + def solve(puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle sequentially without RSA private parameters. + + This implementation: + 1. Calculates 2^t directly + 2. Then computes x^(2^t) mod N in one step using powmod + + Args: + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ + x = puzzle.get_x() + N = puzzle.get_N() + t = puzzle.get_t() + + # Calculate 2^t first + exp = MPC.pow(TWO, t) + + # Then calculate x^(2^t) mod N in one step + return MPC.powmod(x, exp, N) diff --git a/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzle.py b/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzle.py new file mode 100644 index 0000000..65a0a51 --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzle.py @@ -0,0 +1,27 @@ +from ..mpc.types import MPZ +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle + + +class TimeLockPuzzle(ITimeLockPuzzle): + """Implementation of a time lock puzzle.""" + + def __init__(self, x: MPZ, t: MPZ, N: MPZ) -> None: + """Initialize a time lock puzzle. + + Args: + x (MPZ): The input value + t (MPZ): The time parameter + N (MPZ): The modulus + """ + self._x = x + self._t = t + self._N = N + + def get_x(self) -> MPZ: + return self._x + + def get_t(self) -> MPZ: + return self._t + + def get_N(self) -> MPZ: + return self._N diff --git a/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleBuilder.py b/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleBuilder.py new file mode 100644 index 0000000..be6e9da --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleBuilder.py @@ -0,0 +1,30 @@ +from typing import Self +from ..mpc.types import MPZ +from .TimeLockPuzzle import TimeLockPuzzle +from .abstract.ITimeLockPuzzleBuilder import ITimeLockPuzzleBuilder + + +class TimeLockPuzzleBuilder(ITimeLockPuzzleBuilder): + """Implementation of time lock puzzle builder.""" + + def __init__(self) -> None: + self._x = None + self._t = None + self._N = None + + def set_x(self, x: MPZ) -> Self: + self._x = x + return self + + def set_t(self, t: MPZ) -> Self: + self._t = t + return self + + def set_N(self, N: MPZ) -> Self: + self._N = N + return self + + def build(self) -> TimeLockPuzzle: + if self._x is None or self._t is None or self._N is None: + raise ValueError("All parameters (x, t, N) must be set before building") + return TimeLockPuzzle(self._x, self._t, self._N) diff --git a/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleFactory.py b/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleFactory.py new file mode 100644 index 0000000..19380a2 --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleFactory.py @@ -0,0 +1,78 @@ +from typing import List, Tuple +import multiprocessing + +from src.time_lock_puzzle import TimeLockPuzzleBuilder +from ..mpc import MPC +from ..mpc.types import MPZ +from ..random import Random +from ..rsa.RSA import RSA +from .TimeLockPuzzle import TimeLockPuzzle +from .EfficientTimeLockPuzzleSolver import EfficientTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzleFactory import ITimeLockPuzzleFactory + + +class TimeLockPuzzleFactory(ITimeLockPuzzleFactory): + """Implementation of time lock puzzle factory.""" + + def __init__(self, bit_size: int, timing_parameter: MPZ) -> None: + """Initialize the factory. + + Args: + bit_size (int): Number of bits for RSA parameters + timing_parameter (MPZ): Time parameter t for puzzles + """ + self._bit_size = bit_size + self._t = timing_parameter + + def create_puzzle(self) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + # Create RSA instance + rsa_instance = RSA(self._bit_size) + + # Generate random x + rand = Random.get_random(self._bit_size) + x = MPC.mpz_urandomb(rand, self._bit_size) + + # Create puzzle using builder + puzzle = ( + TimeLockPuzzleBuilder() + .set_x(x) + .set_t(self._t) + .set_N(rsa_instance.get_N()) + .build() + ) + + # Get solution using efficient solver + y = EfficientTimeLockPuzzleSolver.solve(rsa_instance, puzzle) + + return puzzle, rsa_instance, y + + def create_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + # Create parameters for each puzzle + puzzle_params = [(self._bit_size, self._t) for _ in range(amount)] + + # Create puzzles in parallel using process pool + with multiprocessing.Pool() as pool: + puzzles = pool.map( + TimeLockPuzzleFactory._create_puzzle_parallel, puzzle_params + ) + + return puzzles + + # Private Methods + # ------------------------------------------------------------------------------ + + @staticmethod + def _create_puzzle_parallel( + puzzle_params: Tuple[int, MPZ], + ) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + """Helper method to create a single puzzle tuple for multiprocessing. + + Args: + puzzle_params (Tuple[int, MPZ]): Tuple containing (bit_size, timing_parameter) + + Returns: + Tuple[TimeLockPuzzle, RSA, MPZ]: A tuple containing the puzzle, RSA instance, and solution + """ + bit_size, t = puzzle_params + factory = TimeLockPuzzleFactory(bit_size, t) + return factory.create_puzzle() diff --git a/verifiable-delay-function/src/time_lock_puzzle/__init__.py b/verifiable-delay-function/src/time_lock_puzzle/__init__.py new file mode 100644 index 0000000..33481c8 --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/__init__.py @@ -0,0 +1,25 @@ +"""Time lock puzzle module.""" + +from .TimeLockPuzzle import TimeLockPuzzle +from .TimeLockPuzzleBuilder import TimeLockPuzzleBuilder +from .TimeLockPuzzleFactory import TimeLockPuzzleFactory +from .EfficientTimeLockPuzzleSolver import EfficientTimeLockPuzzleSolver +from .SequentialTimeLockPuzzleSolver import SequentialTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .abstract.ITimeLockPuzzleBuilder import ITimeLockPuzzleBuilder +from .abstract.ITimeLockPuzzleFactory import ITimeLockPuzzleFactory +from .abstract.IEfficientTimeLockPuzzleSolver import IEfficientTimeLockPuzzleSolver +from .abstract.ISequentialTimeLockPuzzleSolver import ISequentialTimeLockPuzzleSolver + +__all__ = [ + "TimeLockPuzzle", + "TimeLockPuzzleBuilder", + "TimeLockPuzzleFactory", + "EfficientTimeLockPuzzleSolver", + "SequentialTimeLockPuzzleSolver", + "ITimeLockPuzzle", + "ITimeLockPuzzleBuilder", + "ITimeLockPuzzleFactory", + "IEfficientTimeLockPuzzleSolver", + "ISequentialTimeLockPuzzleSolver", +] diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py b/verifiable-delay-function/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py new file mode 100644 index 0000000..f4b66df --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple +from ...mpc.types import MPZ +from ...rsa import RSA +from .ITimeLockPuzzle import ITimeLockPuzzle + + +class IEfficientTimeLockPuzzleSolver(ABC): + """Abstract base class defining the interface for an efficient time lock puzzle solver.""" + + @staticmethod + @abstractmethod + def solve(rsa: RSA, puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle efficiently using RSA private parameters. + + Args: + rsa (RSA): The RSA instance with private parameters + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ + pass + + @staticmethod + @abstractmethod + def solve_many(puzzles: List[Tuple[RSA, ITimeLockPuzzle]]) -> List[MPZ]: + """Solve multiple time lock puzzles in parallel using multiprocessing. + + Args: + puzzles: List of tuples containing (RSA, puzzle) pairs to solve + + Returns: + List[MPZ]: List of solutions in the same order as input puzzles + """ + pass diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py b/verifiable-delay-function/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py new file mode 100644 index 0000000..c8f6889 --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ +from .ITimeLockPuzzle import ITimeLockPuzzle + + +class ISequentialTimeLockPuzzleSolver(ABC): + """Abstract base class defining the interface for a sequential time lock puzzle solver.""" + + @staticmethod + @abstractmethod + def solve(puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle sequentially without RSA private parameters. + + Args: + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py b/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py new file mode 100644 index 0000000..74b748d --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class ITimeLockPuzzle(ABC): + """Abstract base class defining the interface for a time lock puzzle implementation.""" + + @abstractmethod + def get_x(self) -> MPZ: + """Get the input value x. + + Returns: + MPZ: The input value x + """ + + @abstractmethod + def get_t(self) -> MPZ: + """Get the time parameter t. + + Returns: + MPZ: The time parameter t + """ + + @abstractmethod + def get_N(self) -> MPZ: + """Get the modulus N. + + Returns: + MPZ: The modulus N + """ diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py b/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py new file mode 100644 index 0000000..aa24405 --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py @@ -0,0 +1,49 @@ +from abc import ABC, abstractmethod +from typing import Self +from ...mpc.types import MPZ +from ..TimeLockPuzzle import TimeLockPuzzle + + +class ITimeLockPuzzleBuilder(ABC): + """Abstract base class defining the interface for a time lock puzzle builder.""" + + @abstractmethod + def set_x(self, x: MPZ) -> Self: + """Set the input value x. + + Args: + x (MPZ): The input value + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def set_t(self, t: MPZ) -> Self: + """Set the time parameter t. + + Args: + t (MPZ): The time parameter + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def set_N(self, N: MPZ) -> Self: + """Set the modulus N. + + Args: + N (MPZ): The modulus + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def build(self) -> TimeLockPuzzle: + """Build the time lock puzzle. + + Returns: + TimeLockPuzzle: The constructed puzzle + """ diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py b/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py new file mode 100644 index 0000000..26bb910 --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple +from ...mpc.types import MPZ +from ...rsa import RSA +from ..TimeLockPuzzle import TimeLockPuzzle + + +class ITimeLockPuzzleFactory(ABC): + """Abstract base class defining the interface for a time lock puzzle factory.""" + + @abstractmethod + def create_puzzle(self) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + """Create a new time lock puzzle with solution. + + Returns: + Tuple[TimeLockPuzzle, RSA, MPZ]: A tuple containing: + - The time lock puzzle + - The RSA instance used to create the puzzle + - The solution y + """ + + @abstractmethod + def create_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + """Create multiple time lock puzzles with solutions in parallel. + + Args: + amount (int): Number of puzzles to create + + Returns: + List[Tuple[TimeLockPuzzle, RSA, MPZ]]: A list of tuples, each containing: + - The time lock puzzle + - The RSA instance used to create the puzzle + - The solution y + """ diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/__init__.py b/verifiable-delay-function/src/time_lock_puzzle/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/verifiable-delay-function/src/time_lock_puzzle/constants.py b/verifiable-delay-function/src/time_lock_puzzle/constants.py new file mode 100644 index 0000000..acfd082 --- /dev/null +++ b/verifiable-delay-function/src/time_lock_puzzle/constants.py @@ -0,0 +1,5 @@ +"""Constants for time lock puzzle module.""" + +from ..mpc import MPC + +TWO = MPC.mpz(2) diff --git a/verifiable-delay-function/src/verifiable_delay_function/verifiable_delay_function.py b/verifiable-delay-function/src/verifiable_delay_function/verifiable_delay_function.py index 3412628..57cd0e9 100644 --- a/verifiable-delay-function/src/verifiable_delay_function/verifiable_delay_function.py +++ b/verifiable-delay-function/src/verifiable_delay_function/verifiable_delay_function.py @@ -1,21 +1,26 @@ -import gmpy2 -from gmpy2 import mpz, powmod, random_state, mpz_urandomb +"""File for Verifiable Delay Functions Class""" + from typing import Tuple, List from multiprocessing import Pool import secrets +import gmpy2 + +from gmpy2 import mpz, powmod, random_state, mpz_urandomb class VerifiableDelayFunction: def __init__(self, bit_size: int = 2048, T: int = 1000, num_segments: int = 10): # Check if T is divisible by num_segments if T % num_segments != 0: - raise ValueError(f"The total number of squarings (T={T}) must be divisible by the number of segments (num_segments={num_segments}).") - + raise ValueError( + f"The total number of squarings (T={T}) must be divisible by the number of segments (num_segments={num_segments})." + ) + self.bit_size = bit_size self.T = T self.num_segments = num_segments self.segment_length = T // num_segments - + # Seed with a cryptographically secure random integer secure_seed = secrets.randbits(256) self.rand = random_state(secure_seed) @@ -30,12 +35,16 @@ def evaluate(self) -> mpz: self.proof = [] # Instead of squaring `segment_length` times, use exponentiation - segment_exp = 2 ** self.segment_length + segment_exp = 2**self.segment_length # Iterate over each segment and apply the batched exponentiation for segment in range(self.num_segments): - result = powmod(result, segment_exp, self.N) # Exponentiate by 2^segment_length in a single step - self.proof.append(mpz(result)) # Store the intermediate result as part of the proof + result = powmod( + result, segment_exp, self.N + ) # Exponentiate by 2^segment_length in a single step + self.proof.append( + mpz(result) + ) # Store the intermediate result as part of the proof self.y = result return result @@ -61,7 +70,7 @@ def verify_segment(args: Tuple[mpz, mpz, int, mpz]) -> bool: for _ in range(segment_length): result = powmod(result, 2, N) return result == end_value - + def parallel_verify(self, y: mpz, proof: List[mpz]) -> bool: """ Performs parallel verification by checking each proof segment concurrently @@ -79,11 +88,11 @@ def parallel_verify(self, y: mpz, proof: List[mpz]) -> bool: (proof[i - 1] if i > 0 else self.x, proof[i], self.segment_length, self.N) for i in range(len(proof)) ] - + # Step 2: Use multiprocessing Pool to verify each segment in parallel with Pool() as pool: results = pool.map(self.verify_segment, tasks) - + # Step 3: Check if all segments verified successfully if not all(results): print("Parallel verification failed.") @@ -91,15 +100,16 @@ def parallel_verify(self, y: mpz, proof: List[mpz]) -> bool: # Final check: Verify that the last computed segment result matches y return proof[-1] == y + ##Private## def _generate_rsa_modulus(self) -> Tuple[mpz, mpz, mpz]: while True: # Generate p and q with slightly fewer bits p = gmpy2.next_prime(mpz_urandomb(self.rand, self.bit_size // 2 - 1)) q = gmpy2.next_prime(mpz_urandomb(self.rand, self.bit_size // 2 - 1)) - + N = p * q - + # Check if N is within the desired bit size if N.bit_length() <= self.bit_size: return N, p, q diff --git a/verifiable-delay-function/tests/converters/test_time_lock_puzzle_converter.py b/verifiable-delay-function/tests/converters/test_time_lock_puzzle_converter.py deleted file mode 100644 index 5fbacbc..0000000 --- a/verifiable-delay-function/tests/converters/test_time_lock_puzzle_converter.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest -from unittest.mock import patch -from gmpy2 import mpz -from typing import List -from src.verifiable_delay_function.verifiable_delay_function import VerifiableDelayFunction -from src.database.entity.verifiable_delay_function_entity import VerifiableDelayFunctionEntity -from src.converters.verifiable_delay_function_converter import conver_verifiable_delay_function_to_entity # Adjust the import path if needed - -@pytest.fixture -def mocked_verifiable_delay_function(): - """Fixture to create a VerifiableDelayFunction instance with predefined values for testing.""" - with patch.object(VerifiableDelayFunction, '_generate_rsa_modulus') as mock_modulus, \ - patch.object(VerifiableDelayFunction, '_generate_random_challenge') as mock_challenge: - - # Set known values for N, p, q, and x - mock_modulus.return_value = (mpz(21), mpz(7), mpz(3)) # Small modulus for test - mock_challenge.return_value = mpz(5) # Known value for x - - # Instantiate VerifiableDelayFunction with smaller parameters for quicker tests - puzzle = VerifiableDelayFunction(bit_size=16, T=2, num_segments=1) - puzzle.evaluate() # Generate y and proof based on mocked values - return puzzle - -def test_convert_verifiable_delay_function_to_entity(mocked_verifiable_delay_function): - """Test conversion of a VerifiableDelayFunction instance to VerifiableDelayFunctionEntity with hex string storage.""" - # Perform the conversion - entity: VerifiableDelayFunctionEntity = conver_verifiable_delay_function_to_entity(mocked_verifiable_delay_function) - - # Expected values based on the mocked puzzle's modulus, x, y, and proof - expected_modulus = mocked_verifiable_delay_function.N.digits(16) - expected_input = mocked_verifiable_delay_function.x.digits(16) - expected_output = mocked_verifiable_delay_function.y.digits(16) - expected_proof = [p.digits(16) for p in mocked_verifiable_delay_function.proof] - - # Assertions - assert isinstance(entity, VerifiableDelayFunctionEntity) - assert entity.modulus == expected_modulus - assert entity.input == expected_input - assert entity.output == expected_output - assert entity.proof == expected_proof - -def test_convert_verifiable_delay_function_to_entity_missing_proof(): - """Test that conver_verifiable_delay_function_to_entity raises an error if y or proof is missing.""" - # Create a VerifiableDelayFunction instance - vdf_instance = VerifiableDelayFunction(bit_size=16, T=2, num_segments=1) - - # Do not run evaluate() to keep y and proof unset - # Attempt conversion, expecting a ValueError - with pytest.raises(ValueError, match="The VDF instance must be evaluated and proofed before conversion."): - conver_verifiable_delay_function_to_entity(vdf_instance) \ No newline at end of file diff --git a/verifiable-delay-function/tests/database/entity/test_time_lock_puzzle_entity.py b/verifiable-delay-function/tests/database/entity/test_time_lock_puzzle_entity.py deleted file mode 100644 index fb45b34..0000000 --- a/verifiable-delay-function/tests/database/entity/test_time_lock_puzzle_entity.py +++ /dev/null @@ -1,58 +0,0 @@ -import pytest -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from src.database.database import get_orm_base -from src.database.entity.verifiable_delay_function_entity import VerifiableDelayFunctionEntity - -# Setup in-memory SQLite database for testing -@pytest.fixture(scope="module") -def test_database(): - # Create an in-memory SQLite database engine - engine = create_engine("sqlite:///:memory:") - # Bind the base to this engine - Base = get_orm_base() - Base.metadata.create_all(engine) # Create tables - - # Create a sessionmaker bound to this engine - Session = sessionmaker(bind=engine) - session = Session() - - yield session # Provide the session to tests - - # Teardown: close session and drop tables - session.close() - Base.metadata.drop_all(engine) - -def test_verifiable_delay_function_entity_save(test_database): - """Test the saving functionality of VerifiableDelayFunctionEntity with hex strings.""" - # Create a VerifiableDelayFunctioneEntity instance with hex strings - entity = VerifiableDelayFunctionEntity( - modulus_hex='010203', # Example modulus hex string - input_hex='0405', # Example input hex string - output_hex='0607', # Example output hex string - proof=['proof_segment_1', 'proof_segment_2'] - ) - - # Save the entity to the database - test_database.add(entity) - test_database.commit() - - # Verify that the entity was saved and assigned an ID - saved_entity = test_database.query(VerifiableDelayFunctionEntity).filter_by(id=entity.id).first() - assert saved_entity is not None, "Entity was not saved." - assert saved_entity.id == entity.id - assert saved_entity.modulus == '010203' - assert saved_entity.input == '0405' - assert saved_entity.output == '0607' - assert saved_entity.proof == ['proof_segment_1', 'proof_segment_2'] - -def test_verifiable_delay_function_entity_repr(): - """Test that the __repr__ output of VerifiableDelayFunctionEntity is not empty.""" - entity = VerifiableDelayFunctionEntity( - modulus_hex='010203', # Example modulus hex string - input_hex='0405', # Example input hex string - output_hex='0607', # Example output hex string - proof=['proof_segment_1', 'proof_segment_2'] - ) - repr_output = repr(entity) - assert repr_output, "The __repr__ output is empty." \ No newline at end of file diff --git a/verifiable-delay-function/tests/time_lock_puzzle/test_time_lock_puzzle_basic_functionality.py b/verifiable-delay-function/tests/time_lock_puzzle/test_time_lock_puzzle_basic_functionality.py deleted file mode 100644 index 5244c7d..0000000 --- a/verifiable-delay-function/tests/time_lock_puzzle/test_time_lock_puzzle_basic_functionality.py +++ /dev/null @@ -1,45 +0,0 @@ - -import pytest -from gmpy2 import mpz - - -from src.verifiable_delay_function.verifiable_delay_function import VerifiableDelayFunction - - -@pytest.fixture -def verifiable_delay_function_instance(): - """Fixture to create a VerifiableDelayFunction instance.""" - return VerifiableDelayFunction(bit_size=512, T=100, num_segments=5) # Smaller size for quicker testing - -def test_initialization(verifiable_delay_function_instance): - """Test that VerifiableDelayFunction initializes properly.""" - assert verifiable_delay_function_instance.bit_size == 512 - assert verifiable_delay_function_instance.T == 100 - assert verifiable_delay_function_instance.num_segments == 5 - assert verifiable_delay_function_instance.segment_length == 20 # T // num_segments - -def test_evaluate(verifiable_delay_function_instance): - """Test that the evaluate method runs and produces an expected type and proof segments.""" - y = verifiable_delay_function_instance.evaluate() - assert isinstance(y, mpz) - assert len(verifiable_delay_function_instance.proof) == verifiable_delay_function_instance.num_segments - -def test_generate_proof(verifiable_delay_function_instance): - """Test that generate_proof produces the correct output.""" - y, proof = verifiable_delay_function_instance.generate_proof() - assert isinstance(y, mpz) - assert isinstance(proof, list) - assert len(proof) == verifiable_delay_function_instance.num_segments - -def test_parallel_verify(verifiable_delay_function_instance): - """Test that parallel_verify correctly verifies the generated proof.""" - y, proof = verifiable_delay_function_instance.generate_proof() - assert verifiable_delay_function_instance.parallel_verify(y, proof) - -def test_non_divisible_segments_error(): - """Test that VerifiableDelayFunction raises an error when T is not divisible by num_segments.""" - T = 7 - num_segments = 3 - - with pytest.raises(ValueError): - VerifiableDelayFunction(T=T, num_segments=num_segments) \ No newline at end of file diff --git a/verifiable-delay-function/tests/time_lock_puzzle/test_time_lock_puzzle_sample.py b/verifiable-delay-function/tests/time_lock_puzzle/test_time_lock_puzzle_sample.py deleted file mode 100644 index 5117847..0000000 --- a/verifiable-delay-function/tests/time_lock_puzzle/test_time_lock_puzzle_sample.py +++ /dev/null @@ -1,120 +0,0 @@ -import pytest -from unittest.mock import patch -from gmpy2 import mpz -import pytest -from unittest.mock import patch -from gmpy2 import mpz -from src.verifiable_delay_function.verifiable_delay_function import VerifiableDelayFunction - -@pytest.fixture -def verifiable_delay_function_instance(): - """Fixture to create a VerifiableDelayFunction instance with mocked values.""" - with patch.object(VerifiableDelayFunction, '_generate_rsa_modulus') as mock_modulus, \ - patch.object(VerifiableDelayFunction, '_generate_random_challenge') as mock_challenge: - - # Set known values for N, p, q, and x - mock_modulus.return_value = (mpz(21), mpz(7), mpz(3)) # Small modulus for test - mock_challenge.return_value = mpz(5) # Known value for x - - # Instantiate VerifiableDelayFunction with smaller parameters for quicker tests - puzzle = VerifiableDelayFunction(bit_size=16, T=2, num_segments=1) - return puzzle - -def test_evaluate(verifiable_delay_function_instance): - """Test the evaluate method with fixed values for N and x.""" - y = verifiable_delay_function_instance.evaluate() - - # Expected proof for this known N, x, T, and num_segments - # Calculation: x^2^T mod N - # First squaring: 5^2 = 25, then 25 mod 21 = 4 - # Second squaring: 4^2 = 16, then 16 mod 21 = 16 - expected_proof = [mpz(16)] - expected_y = mpz(16) - - assert y == expected_y # Final y should match last proof segment - assert verifiable_delay_function_instance.proof == expected_proof - -def test_generate_proof(verifiable_delay_function_instance): - """Test generate_proof to ensure it matches the expected output.""" - y, proof = verifiable_delay_function_instance.generate_proof() - - # Expected proof for this known N, x, T, and num_segments - expected_proof = [mpz(16)] - expected_y = mpz(16) - - assert y == expected_y - assert proof == expected_proof - -def test_parallel_verify(verifiable_delay_function_instance): - """Test parallel_verify with the mocked values to ensure verification passes.""" - y, proof = verifiable_delay_function_instance.generate_proof() - assert verifiable_delay_function_instance.parallel_verify(y, proof) # Verification should succeed - -@pytest.fixture -def create_verifiable_delay_function_instance(): - """Factory fixture to create VerifiableDelayFunction instances with varied parameters.""" - def _create_instance(T, num_segments): - with patch.object(VerifiableDelayFunction, '_generate_rsa_modulus') as mock_modulus, \ - patch.object(VerifiableDelayFunction, '_generate_random_challenge') as mock_challenge: - - # Set known values for N, p, q, and x - mock_modulus.return_value = (mpz(21), mpz(7), mpz(3)) # Small modulus for test - mock_challenge.return_value = mpz(5) # Known value for x - - # Instantiate VerifiableDelayFunction with specified T and num_segments - puzzle = VerifiableDelayFunction(bit_size=16, T=T, num_segments=num_segments) - return puzzle - return _create_instance - -def test_evaluate_single_segment(create_verifiable_delay_function_instance): - """Test the evaluate method with a single segment (num_segments=1).""" - puzzle = create_verifiable_delay_function_instance(T=4, num_segments=1) - y = puzzle.evaluate() - - # Calculation for T=4, single segment - # First squaring: 5^2 = 25, then 25 mod 21 = 4 - # Second squaring: 4^2 = 16, then 16 mod 21 = 16 - # Third squaring: 16^2 = 256, then 256 mod 21 = 4 - # Fourth squaring: 4^2 = 16, then 16 mod 21 = 16 - expected_proof = [mpz(16)] - expected_y = mpz(16) - - assert y == expected_y - assert puzzle.proof == expected_proof - -def test_evaluate_multiple_segments(create_verifiable_delay_function_instance): - """Test the evaluate method with multiple segments (num_segments=2).""" - puzzle = create_verifiable_delay_function_instance(T=4, num_segments=2) - y = puzzle.evaluate() - - # Expected proof for T=4, num_segments=2, segment_length=2 - expected_proof = [mpz(16), mpz(16)] - expected_y = mpz(16) - - assert y == expected_y - assert puzzle.proof == expected_proof - -def test_generate_proof_varying_segments(create_verifiable_delay_function_instance): - """Test generate_proof with varying segments.""" - # Test with T=6 and num_segments=3 (segment_length=2) - puzzle = create_verifiable_delay_function_instance(T=6, num_segments=3) - y, proof = puzzle.generate_proof() - - # Expected proof segments based on calculations - expected_proof = [mpz(16), mpz(16), mpz(16)] - expected_y = mpz(16) - - assert y == expected_y - assert proof == expected_proof - -def test_parallel_verify_varying_segments(create_verifiable_delay_function_instance): - """Test parallel_verify with varying segments to ensure verification passes.""" - # Test with T=6 and num_segments=3 - puzzle = create_verifiable_delay_function_instance(T=6, num_segments=3) - y, proof = puzzle.generate_proof() - assert puzzle.parallel_verify(y, proof) # Verification should succeed - - # Test with T=4 and num_segments=2 - puzzle = create_verifiable_delay_function_instance(T=4, num_segments=2) - y, proof = puzzle.generate_proof() - assert puzzle.parallel_verify(y, proof) # Verification should succeed From 70a11b0733dd2643c13de485d23c9d650d3194df Mon Sep 17 00:00:00 2001 From: K <111819113+KennySwayzee93@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:09:45 -0400 Subject: [PATCH 35/80] Time lock puzzles and database and linkage all setup --- .../.env.example | 0 .../.gitignore | 0 .../.pylintrc | 0 .../Dockerfile | 70 +++++------ puzzle-generator/README.md | 27 ++++ .../conftest.py | 0 .../docs/developing.md | 6 +- .../main.py | 12 +- .../requirements.txt | 0 .../src/__init__.py | 0 .../src/converters/__init__.py | 1 - .../src/converters/rsa_converter.py | 0 .../converters/time_lock_puzzle_converter.py | 9 +- .../src/database/DatabaseService.py | 0 .../src/database/__init__.py | 0 .../src/database/constants.py | 0 .../src/database/database.py | 0 .../src/database/entity/RSAEntity.py | 11 +- .../database/entity/TimeLockPuzzleEntity.py | 17 ++- .../src/database/entity/__init__.py | 6 + .../src/database/initialize_db.py | 8 +- .../src/database/mixins/__init__.py | 0 .../src/database/mixins/saveable.py | 0 .../src/mpc/MPC.py | 0 .../src/mpc/__init__.py | 0 .../src/mpc/abstract/IMPC.py | 0 .../src/mpc/abstract/__init__.py | 0 .../src/mpc/types.py | 0 .../src/primes/Primes.py | 0 .../src/primes/__init__.py | 0 .../src/primes/abstract/IPrimes.py | 0 .../src/primes/abstract/__init__.py | 0 puzzle-generator/src/protocol_constants.py | 7 ++ .../src/random/Random.py | 0 .../src/random/__init__.py | 0 .../src/random/abstract/IRandom.py | 0 .../src/random/abstract/__init__.py | 0 .../src/rsa/RSA.py | 0 .../src/rsa/__init__.py | 0 .../src/rsa/abstract/IRSA.py | 0 .../src/rsa/abstract/__init__.py | 0 .../EfficientTimeLockPuzzleSolver.py | 0 .../SequentialTimeLockPuzzleSolver.py | 0 .../src/time_lock_puzzle/TimeLockPuzzle.py | 0 .../time_lock_puzzle/TimeLockPuzzleBuilder.py | 0 .../time_lock_puzzle/TimeLockPuzzleFactory.py | 0 .../src/time_lock_puzzle/__init__.py | 0 .../IEfficientTimeLockPuzzleSolver.py | 0 .../ISequentialTimeLockPuzzleSolver.py | 0 .../abstract/ITimeLockPuzzle.py | 0 .../abstract/ITimeLockPuzzleBuilder.py | 0 .../abstract/ITimeLockPuzzleFactory.py | 0 .../src/time_lock_puzzle/abstract/__init__.py | 0 .../src/time_lock_puzzle/constants.py | 0 verifiable-delay-function/README.md | 26 ---- verifiable-delay-function/main.py | 42 ------- .../verifiable_delay_function_converter.py | 34 ----- .../src/database/entity/__init__.py | 7 -- .../verifiable_delay_function_entity.py | 29 ----- .../src/protocol_constants.py | 5 - .../src/verifiable_delay_function/__init__.py | 0 .../verifiable_delay_function.py | 118 ------------------ 62 files changed, 119 insertions(+), 316 deletions(-) rename {verifiable-delay-function => puzzle-generator}/.env.example (100%) rename {verifiable-delay-function => puzzle-generator}/.gitignore (100%) rename {verifiable-delay-function => puzzle-generator}/.pylintrc (100%) rename {verifiable-delay-function => puzzle-generator}/Dockerfile (96%) create mode 100644 puzzle-generator/README.md rename {verifiable-delay-function => puzzle-generator}/conftest.py (100%) rename {verifiable-delay-function => puzzle-generator}/docs/developing.md (83%) rename verifiable-delay-function/main_time_lock_puzzle.py => puzzle-generator/main.py (91%) rename {verifiable-delay-function => puzzle-generator}/requirements.txt (100%) rename {verifiable-delay-function => puzzle-generator}/src/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/converters/__init__.py (79%) rename {verifiable-delay-function => puzzle-generator}/src/converters/rsa_converter.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/converters/time_lock_puzzle_converter.py (64%) rename {verifiable-delay-function => puzzle-generator}/src/database/DatabaseService.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/database/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/database/constants.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/database/database.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/database/entity/RSAEntity.py (77%) rename {verifiable-delay-function => puzzle-generator}/src/database/entity/TimeLockPuzzleEntity.py (60%) create mode 100644 puzzle-generator/src/database/entity/__init__.py rename {verifiable-delay-function => puzzle-generator}/src/database/initialize_db.py (86%) rename {verifiable-delay-function => puzzle-generator}/src/database/mixins/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/database/mixins/saveable.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/mpc/MPC.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/mpc/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/mpc/abstract/IMPC.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/mpc/abstract/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/mpc/types.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/primes/Primes.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/primes/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/primes/abstract/IPrimes.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/primes/abstract/__init__.py (100%) create mode 100644 puzzle-generator/src/protocol_constants.py rename {verifiable-delay-function => puzzle-generator}/src/random/Random.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/random/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/random/abstract/IRandom.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/random/abstract/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/rsa/RSA.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/rsa/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/rsa/abstract/IRSA.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/rsa/abstract/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/TimeLockPuzzle.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/TimeLockPuzzleBuilder.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/TimeLockPuzzleFactory.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/abstract/__init__.py (100%) rename {verifiable-delay-function => puzzle-generator}/src/time_lock_puzzle/constants.py (100%) delete mode 100644 verifiable-delay-function/README.md delete mode 100644 verifiable-delay-function/main.py delete mode 100644 verifiable-delay-function/src/converters/verifiable_delay_function_converter.py delete mode 100644 verifiable-delay-function/src/database/entity/__init__.py delete mode 100644 verifiable-delay-function/src/database/entity/verifiable_delay_function_entity.py delete mode 100644 verifiable-delay-function/src/protocol_constants.py delete mode 100644 verifiable-delay-function/src/verifiable_delay_function/__init__.py delete mode 100644 verifiable-delay-function/src/verifiable_delay_function/verifiable_delay_function.py diff --git a/verifiable-delay-function/.env.example b/puzzle-generator/.env.example similarity index 100% rename from verifiable-delay-function/.env.example rename to puzzle-generator/.env.example diff --git a/verifiable-delay-function/.gitignore b/puzzle-generator/.gitignore similarity index 100% rename from verifiable-delay-function/.gitignore rename to puzzle-generator/.gitignore diff --git a/verifiable-delay-function/.pylintrc b/puzzle-generator/.pylintrc similarity index 100% rename from verifiable-delay-function/.pylintrc rename to puzzle-generator/.pylintrc diff --git a/verifiable-delay-function/Dockerfile b/puzzle-generator/Dockerfile similarity index 96% rename from verifiable-delay-function/Dockerfile rename to puzzle-generator/Dockerfile index 1064f5f..9a05905 100644 --- a/verifiable-delay-function/Dockerfile +++ b/puzzle-generator/Dockerfile @@ -1,35 +1,35 @@ -# Use an official Python image as a base -FROM python:3.8-slim - -# Install system dependencies needed for gmpy2 and PostgreSQL connection -RUN apt-get update && apt-get install -y \ - libgmp-dev \ - libmpfr-dev \ - libmpc-dev \ - gcc \ - && rm -rf /var/lib/apt/lists/* - -# Set up a working directory -WORKDIR /app - -# Copy only the requirements file to leverage Docker cache -COPY requirements.txt . - -# Install Python dependencies -RUN pip install --no-cache-dir -r requirements.txt - -# Copy the rest of the application code -COPY . . - -# Set environment variables for PostgreSQL credentials (can be overridden at runtime) -ENV DB_NAME=mydatabase \ - DB_USER=myuser \ - DB_PASSWORD=mypassword \ - DB_HOST=localhost \ - DB_PORT=5432 - -# Expose any necessary ports (optional, specify if your app uses specific ports) -# EXPOSE 8000 - -# Command to run the main script -# CMD ["python", "main.py"] +# Use an official Python image as a base +FROM python:3.8-slim + +# Install system dependencies needed for gmpy2 and PostgreSQL connection +RUN apt-get update && apt-get install -y \ + libgmp-dev \ + libmpfr-dev \ + libmpc-dev \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Set up a working directory +WORKDIR /app + +# Copy only the requirements file to leverage Docker cache +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code +COPY . . + +# Set environment variables for PostgreSQL credentials (can be overridden at runtime) +ENV DB_NAME=mydatabase \ + DB_USER=myuser \ + DB_PASSWORD=mypassword \ + DB_HOST=localhost \ + DB_PORT=5432 + +# Expose any necessary ports (optional, specify if your app uses specific ports) +# EXPOSE 8000 + +# Command to run the main script +# CMD ["python", "main.py"] diff --git a/puzzle-generator/README.md b/puzzle-generator/README.md new file mode 100644 index 0000000..687e29f --- /dev/null +++ b/puzzle-generator/README.md @@ -0,0 +1,27 @@ +# [🔙](../) Time-Lock Puzzles +This repository section contains an implementation of [Time-Lock Puzzles](https://en.wikipedia.org/wiki/Time-lock_puzzle) as outlined in the seminal paper [Time-lock puzzles and timed-release Crypto](https://people.csail.mit.edu/rivest/pubs/RSW96.pdf) by Ronald L. Rivest, Adi Shamir, and David A. Wagner. + +This Time-Lock Puzzle implementation is part of **RandAO's Randomness Provider** project, designed to provide a reliable source of randomness based on cryptographic time delays. RandAO's Randomness Provider leverages Time-Lock Puzzles to ensure that randomness generation requires a precise amount of sequential computation time, establishing trust and security for applications requiring provably delayed randomness. + +## Table of Contents +- [Overview](#overview) +- [Development](#development) +- [License](#license) + +## Overview +The Time-Lock Puzzle implementation in this repository follows the specifications in the [RSW96 paper](https://people.csail.mit.edu/rivest/pubs/RSW96.pdf), providing a cryptographically secure mechanism for creating puzzles that require a predetermined amount of sequential computation to solve. This feature is crucial for applications in time-released cryptography and decentralized randomness protocols, where it is essential to produce randomness that cannot be accessed before a specific time has elapsed. + +Key features of this Time-Lock Puzzle implementation include: + + - Sequential Computation: The puzzle's design requires a specific number of sequential squaring operations modulo a composite number, ensuring that parallel computing offers no advantage in solving the puzzle. + - Precise Time Calibration: The difficulty of each puzzle can be precisely calibrated based on the computing power available to the solver. + - Efficient Creation: Puzzles can be created efficiently by anyone who knows the factorization of the modulus. + - Secure Message Encryption: The puzzle can securely encrypt a message that remains hidden until the sequential computation is completed. + +This approach enables decentralized protocols to produce randomness that is guaranteed to remain secret for a specific time period, making it ideal for use cases such as secure time-released cryptography, fair contract signing, sealed-bid auctions, and other applications requiring temporal security guarantees. + +## Development +For detailed development guidelines, including contributing, testing, and documentation, please refer to the [Development Documentation](./docs/developing.md). + +## License +This project is licensed under the MIT License. See the [LICENSE file](../LICENSE) for details. diff --git a/verifiable-delay-function/conftest.py b/puzzle-generator/conftest.py similarity index 100% rename from verifiable-delay-function/conftest.py rename to puzzle-generator/conftest.py diff --git a/verifiable-delay-function/docs/developing.md b/puzzle-generator/docs/developing.md similarity index 83% rename from verifiable-delay-function/docs/developing.md rename to puzzle-generator/docs/developing.md index 6322f61..6cd1ee3 100644 --- a/verifiable-delay-function/docs/developing.md +++ b/puzzle-generator/docs/developing.md @@ -1,5 +1,5 @@ # Project Setup -This guide will walk you through setting up and running the Verifiable Delay Function (VDF) project in Python. +This guide will walk you through setting up and running the Time lock puzzle project in Python. ## Prerequisites - Python 3.7+: Make sure you have Python installed on your system. @@ -39,8 +39,10 @@ python src/database/initialize_db.py ## Running the Project To generate a VDF proof and verify it, run the main.py script: ```bash -python main.py +python main.py 10 ``` +Required Command line Arguments: + - count: the number of time lock puzzles to generate and store in the database ## Running the Tests To run the unit tests, use the following command: diff --git a/verifiable-delay-function/main_time_lock_puzzle.py b/puzzle-generator/main.py similarity index 91% rename from verifiable-delay-function/main_time_lock_puzzle.py rename to puzzle-generator/main.py index dc134a7..a7d10d7 100644 --- a/verifiable-delay-function/main_time_lock_puzzle.py +++ b/puzzle-generator/main.py @@ -11,14 +11,11 @@ from src.database.entity.TimeLockPuzzleEntity import TimeLockPuzzleEntity from src.mpc import MPC from src.mpc.types import MPZ +from src.protocol_constants import BIT_SIZE, TIMING_PARAMETER from src.rsa.RSA import RSA from src.time_lock_puzzle.TimeLockPuzzle import TimeLockPuzzle from src.time_lock_puzzle.TimeLockPuzzleFactory import TimeLockPuzzleFactory -# Constants -BIT_SIZE = 2048 # Size for RSA parameters -TIMING_PARAMETER = MPC.mpz(3_000_000) # Number of squarings required - class TimeLockPuzzleService: """Service class for managing time lock puzzle operations.""" @@ -68,9 +65,11 @@ def convert_to_entities( print("\nConverting to entities...") start_time = time.time() entities = [] - for puzzle, rsa, _ in puzzles: + for puzzle, rsa, y in puzzles: + # Convert RSA entity first to get its ID (now generated on creation) rsa_entity = self.rsa_converter.to_entity(rsa) - puzzle_entity = self.puzzle_converter.to_entity(puzzle) + # Create puzzle entity with the generated RSA ID + puzzle_entity = self.puzzle_converter.to_entity(puzzle, rsa_entity.id, y) entities.extend([rsa_entity, puzzle_entity]) total_time = time.time() - start_time print(f"Entity conversion took {total_time:.2f} seconds") @@ -85,6 +84,7 @@ def save_entities(self, entities: List[TimeLockPuzzleEntity | RSAEntity]) -> Non """ print("\nSaving to database...") start_time = time.time() + # Now we can save all entities at once since RSA IDs are generated on creation DatabaseService.save_many(entities) total_time = time.time() - start_time print(f"Database save took {total_time:.2f} seconds") diff --git a/verifiable-delay-function/requirements.txt b/puzzle-generator/requirements.txt similarity index 100% rename from verifiable-delay-function/requirements.txt rename to puzzle-generator/requirements.txt diff --git a/verifiable-delay-function/src/__init__.py b/puzzle-generator/src/__init__.py similarity index 100% rename from verifiable-delay-function/src/__init__.py rename to puzzle-generator/src/__init__.py diff --git a/verifiable-delay-function/src/converters/__init__.py b/puzzle-generator/src/converters/__init__.py similarity index 79% rename from verifiable-delay-function/src/converters/__init__.py rename to puzzle-generator/src/converters/__init__.py index 6db91fe..ee1ed54 100644 --- a/verifiable-delay-function/src/converters/__init__.py +++ b/puzzle-generator/src/converters/__init__.py @@ -1,6 +1,5 @@ """Converters for database entities.""" -from .verifiable_delay_function_converter import * from .time_lock_puzzle_converter import TimeLockPuzzleConverter from .rsa_converter import RSAConverter diff --git a/verifiable-delay-function/src/converters/rsa_converter.py b/puzzle-generator/src/converters/rsa_converter.py similarity index 100% rename from verifiable-delay-function/src/converters/rsa_converter.py rename to puzzle-generator/src/converters/rsa_converter.py diff --git a/verifiable-delay-function/src/converters/time_lock_puzzle_converter.py b/puzzle-generator/src/converters/time_lock_puzzle_converter.py similarity index 64% rename from verifiable-delay-function/src/converters/time_lock_puzzle_converter.py rename to puzzle-generator/src/converters/time_lock_puzzle_converter.py index 6abedef..7b789b5 100644 --- a/verifiable-delay-function/src/converters/time_lock_puzzle_converter.py +++ b/puzzle-generator/src/converters/time_lock_puzzle_converter.py @@ -1,24 +1,29 @@ """Converter for time lock puzzle objects.""" -from src.time_lock_puzzle import TimeLockPuzzle +from src.time_lock_puzzle.TimeLockPuzzle import TimeLockPuzzle from src.database.entity.TimeLockPuzzleEntity import TimeLockPuzzleEntity +from src.mpc.types import MPZ class TimeLockPuzzleConverter: """Converter between TimeLockPuzzle and TimeLockPuzzleEntity.""" @staticmethod - def to_entity(puzzle: TimeLockPuzzle) -> TimeLockPuzzleEntity: + def to_entity(puzzle: TimeLockPuzzle, rsa_id: str, y: MPZ) -> TimeLockPuzzleEntity: """Convert a TimeLockPuzzle to a TimeLockPuzzleEntity. Args: puzzle (TimeLockPuzzle): The puzzle to convert + rsa_id (str): ID of the associated RSA entity + y (MPZ): The y value from the puzzle tuple Returns: TimeLockPuzzleEntity: The database entity """ return TimeLockPuzzleEntity( x_hex=hex(puzzle.get_x()), + y_hex=hex(y), t=str(puzzle.get_t()), N_hex=hex(puzzle.get_N()), + rsa_id=rsa_id, ) diff --git a/verifiable-delay-function/src/database/DatabaseService.py b/puzzle-generator/src/database/DatabaseService.py similarity index 100% rename from verifiable-delay-function/src/database/DatabaseService.py rename to puzzle-generator/src/database/DatabaseService.py diff --git a/verifiable-delay-function/src/database/__init__.py b/puzzle-generator/src/database/__init__.py similarity index 100% rename from verifiable-delay-function/src/database/__init__.py rename to puzzle-generator/src/database/__init__.py diff --git a/verifiable-delay-function/src/database/constants.py b/puzzle-generator/src/database/constants.py similarity index 100% rename from verifiable-delay-function/src/database/constants.py rename to puzzle-generator/src/database/constants.py diff --git a/verifiable-delay-function/src/database/database.py b/puzzle-generator/src/database/database.py similarity index 100% rename from verifiable-delay-function/src/database/database.py rename to puzzle-generator/src/database/database.py diff --git a/verifiable-delay-function/src/database/entity/RSAEntity.py b/puzzle-generator/src/database/entity/RSAEntity.py similarity index 77% rename from verifiable-delay-function/src/database/entity/RSAEntity.py rename to puzzle-generator/src/database/entity/RSAEntity.py index ffdf98f..b2e8b79 100644 --- a/verifiable-delay-function/src/database/entity/RSAEntity.py +++ b/puzzle-generator/src/database/entity/RSAEntity.py @@ -1,6 +1,7 @@ import uuid from sqlalchemy import Column, String from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship from src.database.mixins.saveable import Saveable from src.database.database import get_orm_base @@ -12,15 +13,16 @@ class RSAEntity(Base, Saveable): """Database entity for storing RSA parameters.""" - __tablename__ = "rsa_parameters" + __tablename__ = "rsa_keys" - id = Column( - String, primary_key=True, default=lambda: str(uuid.uuid4()) - ) # Unique generated string ID + id = Column(String, primary_key=True) # Unique generated string ID p = Column(String, nullable=False) # Store hex string of prime p q = Column(String, nullable=False) # Store hex string of prime q N = Column(String, nullable=False) # Store hex string of modulus N phi = Column(String, nullable=False) # Store hex string of Euler's totient + puzzle = relationship( + "TimeLockPuzzleEntity", back_populates="rsa", uselist=False + ) # One-to-one back reference to puzzle def __repr__(self): return f"" @@ -34,6 +36,7 @@ def __init__(self, p_hex: str, q_hex: str, N_hex: str, phi_hex: str): N_hex (str): Hex string of modulus N phi_hex (str): Hex string of Euler's totient """ + self.id = str(uuid.uuid4()) # Generate ID on creation self.p = p_hex self.q = q_hex self.N = N_hex diff --git a/verifiable-delay-function/src/database/entity/TimeLockPuzzleEntity.py b/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py similarity index 60% rename from verifiable-delay-function/src/database/entity/TimeLockPuzzleEntity.py rename to puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py index bb3b4ca..2abb780 100644 --- a/verifiable-delay-function/src/database/entity/TimeLockPuzzleEntity.py +++ b/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py @@ -1,5 +1,6 @@ import uuid -from sqlalchemy import Column, String +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.orm import relationship from src.database.mixins.saveable import Saveable from src.database.database import get_orm_base @@ -17,13 +18,23 @@ class TimeLockPuzzleEntity(Base, Saveable): String, primary_key=True, default=lambda: str(uuid.uuid4()) ) # Unique generated string ID x = Column(String, nullable=False) # Store hex string of input value x + y = Column(String, nullable=False) # Store hex string of y value t = Column(String, nullable=False) # Store base 10 string of time parameter t N = Column(String, nullable=False) # Store hex string of modulus N + request_id = Column( + String, nullable=True + ) # Optional associated randomness request id (will be filled within the provider node runtime) + rsa_id = Column( + String, ForeignKey("rsa_keys.id"), nullable=False, unique=True + ) # One-to-one reference to RSA key + rsa = relationship( + "RSAEntity", back_populates="puzzle" + ) # One-to-one relationship to RSA entity def __repr__(self): return f"" - def __init__(self, x_hex: str, t: str, N_hex: str): + def __init__(self, x_hex: str, y_hex: str, t: str, N_hex: str, rsa_id: str): """Initialize a time lock puzzle entity. Args: @@ -32,5 +43,7 @@ def __init__(self, x_hex: str, t: str, N_hex: str): N_hex (str): Hex string of modulus N """ self.x = x_hex + self.y = y_hex self.t = t self.N = N_hex + self.rsa_id = rsa_id diff --git a/puzzle-generator/src/database/entity/__init__.py b/puzzle-generator/src/database/entity/__init__.py new file mode 100644 index 0000000..4f7ba70 --- /dev/null +++ b/puzzle-generator/src/database/entity/__init__.py @@ -0,0 +1,6 @@ +"""Database entity models.""" + +from .TimeLockPuzzleEntity import TimeLockPuzzleEntity +from .RSAEntity import RSAEntity + +__all__ = ["TimeLockPuzzleEntity", "RSAEntity"] diff --git a/verifiable-delay-function/src/database/initialize_db.py b/puzzle-generator/src/database/initialize_db.py similarity index 86% rename from verifiable-delay-function/src/database/initialize_db.py rename to puzzle-generator/src/database/initialize_db.py index b25acb3..fb236c4 100644 --- a/verifiable-delay-function/src/database/initialize_db.py +++ b/puzzle-generator/src/database/initialize_db.py @@ -5,10 +5,11 @@ from sqlalchemy.exc import OperationalError # Dynamically add the `src` directory to `sys.path` -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) +from src.database.entity import * from src.database.database import get_engine, Base -from src.database.entity.verifiable_delay_function_entity import VerifiableDelayFunctionEntity + def initialize_database(): """ @@ -24,6 +25,7 @@ def initialize_database(): finally: engine.dispose() # Close the engine when done + if __name__ == "__main__": - + initialize_database() diff --git a/verifiable-delay-function/src/database/mixins/__init__.py b/puzzle-generator/src/database/mixins/__init__.py similarity index 100% rename from verifiable-delay-function/src/database/mixins/__init__.py rename to puzzle-generator/src/database/mixins/__init__.py diff --git a/verifiable-delay-function/src/database/mixins/saveable.py b/puzzle-generator/src/database/mixins/saveable.py similarity index 100% rename from verifiable-delay-function/src/database/mixins/saveable.py rename to puzzle-generator/src/database/mixins/saveable.py diff --git a/verifiable-delay-function/src/mpc/MPC.py b/puzzle-generator/src/mpc/MPC.py similarity index 100% rename from verifiable-delay-function/src/mpc/MPC.py rename to puzzle-generator/src/mpc/MPC.py diff --git a/verifiable-delay-function/src/mpc/__init__.py b/puzzle-generator/src/mpc/__init__.py similarity index 100% rename from verifiable-delay-function/src/mpc/__init__.py rename to puzzle-generator/src/mpc/__init__.py diff --git a/verifiable-delay-function/src/mpc/abstract/IMPC.py b/puzzle-generator/src/mpc/abstract/IMPC.py similarity index 100% rename from verifiable-delay-function/src/mpc/abstract/IMPC.py rename to puzzle-generator/src/mpc/abstract/IMPC.py diff --git a/verifiable-delay-function/src/mpc/abstract/__init__.py b/puzzle-generator/src/mpc/abstract/__init__.py similarity index 100% rename from verifiable-delay-function/src/mpc/abstract/__init__.py rename to puzzle-generator/src/mpc/abstract/__init__.py diff --git a/verifiable-delay-function/src/mpc/types.py b/puzzle-generator/src/mpc/types.py similarity index 100% rename from verifiable-delay-function/src/mpc/types.py rename to puzzle-generator/src/mpc/types.py diff --git a/verifiable-delay-function/src/primes/Primes.py b/puzzle-generator/src/primes/Primes.py similarity index 100% rename from verifiable-delay-function/src/primes/Primes.py rename to puzzle-generator/src/primes/Primes.py diff --git a/verifiable-delay-function/src/primes/__init__.py b/puzzle-generator/src/primes/__init__.py similarity index 100% rename from verifiable-delay-function/src/primes/__init__.py rename to puzzle-generator/src/primes/__init__.py diff --git a/verifiable-delay-function/src/primes/abstract/IPrimes.py b/puzzle-generator/src/primes/abstract/IPrimes.py similarity index 100% rename from verifiable-delay-function/src/primes/abstract/IPrimes.py rename to puzzle-generator/src/primes/abstract/IPrimes.py diff --git a/verifiable-delay-function/src/primes/abstract/__init__.py b/puzzle-generator/src/primes/abstract/__init__.py similarity index 100% rename from verifiable-delay-function/src/primes/abstract/__init__.py rename to puzzle-generator/src/primes/abstract/__init__.py diff --git a/puzzle-generator/src/protocol_constants.py b/puzzle-generator/src/protocol_constants.py new file mode 100644 index 0000000..15e0067 --- /dev/null +++ b/puzzle-generator/src/protocol_constants.py @@ -0,0 +1,7 @@ +# protocol_constants.py + +from src.mpc import MPC + + +BIT_SIZE = 2048 # RSA modulus bit size +TIMING_PARAMETER = MPC.mpz(3_000_000) # T - Total squarings for delay diff --git a/verifiable-delay-function/src/random/Random.py b/puzzle-generator/src/random/Random.py similarity index 100% rename from verifiable-delay-function/src/random/Random.py rename to puzzle-generator/src/random/Random.py diff --git a/verifiable-delay-function/src/random/__init__.py b/puzzle-generator/src/random/__init__.py similarity index 100% rename from verifiable-delay-function/src/random/__init__.py rename to puzzle-generator/src/random/__init__.py diff --git a/verifiable-delay-function/src/random/abstract/IRandom.py b/puzzle-generator/src/random/abstract/IRandom.py similarity index 100% rename from verifiable-delay-function/src/random/abstract/IRandom.py rename to puzzle-generator/src/random/abstract/IRandom.py diff --git a/verifiable-delay-function/src/random/abstract/__init__.py b/puzzle-generator/src/random/abstract/__init__.py similarity index 100% rename from verifiable-delay-function/src/random/abstract/__init__.py rename to puzzle-generator/src/random/abstract/__init__.py diff --git a/verifiable-delay-function/src/rsa/RSA.py b/puzzle-generator/src/rsa/RSA.py similarity index 100% rename from verifiable-delay-function/src/rsa/RSA.py rename to puzzle-generator/src/rsa/RSA.py diff --git a/verifiable-delay-function/src/rsa/__init__.py b/puzzle-generator/src/rsa/__init__.py similarity index 100% rename from verifiable-delay-function/src/rsa/__init__.py rename to puzzle-generator/src/rsa/__init__.py diff --git a/verifiable-delay-function/src/rsa/abstract/IRSA.py b/puzzle-generator/src/rsa/abstract/IRSA.py similarity index 100% rename from verifiable-delay-function/src/rsa/abstract/IRSA.py rename to puzzle-generator/src/rsa/abstract/IRSA.py diff --git a/verifiable-delay-function/src/rsa/abstract/__init__.py b/puzzle-generator/src/rsa/abstract/__init__.py similarity index 100% rename from verifiable-delay-function/src/rsa/abstract/__init__.py rename to puzzle-generator/src/rsa/abstract/__init__.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py rename to puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py rename to puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzle.py b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzle.py rename to puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleBuilder.py b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleBuilder.py rename to puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleFactory.py b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/TimeLockPuzzleFactory.py rename to puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/__init__.py b/puzzle-generator/src/time_lock_puzzle/__init__.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/__init__.py rename to puzzle-generator/src/time_lock_puzzle/__init__.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py rename to puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py rename to puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py b/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py rename to puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py b/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py rename to puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py b/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py rename to puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/abstract/__init__.py b/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/abstract/__init__.py rename to puzzle-generator/src/time_lock_puzzle/abstract/__init__.py diff --git a/verifiable-delay-function/src/time_lock_puzzle/constants.py b/puzzle-generator/src/time_lock_puzzle/constants.py similarity index 100% rename from verifiable-delay-function/src/time_lock_puzzle/constants.py rename to puzzle-generator/src/time_lock_puzzle/constants.py diff --git a/verifiable-delay-function/README.md b/verifiable-delay-function/README.md deleted file mode 100644 index 59cd891..0000000 --- a/verifiable-delay-function/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# [🔙](../) Verifiable Delay Function (VDF) -This repository section contains an implementation of the [Verifiable Delay Function](https://doi.org/10.4230/LIPIcs.ITCS.2019.60) as outlined by Krzysztof Pietrzak in the paper *Verifiable Delay Functions* (ITCS 2019). - -This VDF implementation is part of **RandAO's Randomness Provider** project, designed to provide a reliable source of randomness based on cryptographic delay. RandAO's Randomness Provider leverages VDFs to ensure that randomness generation is sequential, non-parallelizable, and verifiable, establishing trust and security for applications requiring provably delayed randomness. - -## Table of Contents -- [Overview](#overview) -- [Development](#development) -- [License](#license) - -## Overview -The Verifiable Delay Function (VDF) implemented in this repository follows the specifications in [Pietrzak’s paper](https://doi.org/10.4230/LIPIcs.ITCS.2019.60), providing a cryptographically secure delay mechanism that requires significant serial compute time for generation, yet allows for efficient, parallelized verification. This feature is crucial for applications in decentralized randomness protocols, where it is essential to produce randomness that is both unbiased and verifiable by third parties. - -Key features of this VDF implementation include: - - - Serial Computation for Generation: The VDF’s core design requires sequential calculations to produce the delayed output, ensuring that no shortcut can bypass the intended delay. - - Parallelized Verification: The delayed output is verifiable in a parallelized manner, allowing for efficient proof checks even in distributed environments. - - Secure Random State Initialization: Each VDF instance uses secure, unique seeding for generating the modulus and initial challenge, ensuring cryptographic security across executions. - -This approach enables decentralized protocols to produce and verify randomness that is resistant to tampering or premature access, making it ideal for use cases such as secure lotteries, blockchain protocols, and other decentralized applications requiring provable delay-based randomness. - -## Development -For detailed development guidelines, including contributing, testing, and documentation, please refer to the [Development Documentation](./docs/developing.md). - -## License -This project is licensed under the MIT License. See the [LICENSE file](../LICENSE) for details. \ No newline at end of file diff --git a/verifiable-delay-function/main.py b/verifiable-delay-function/main.py deleted file mode 100644 index f7a4762..0000000 --- a/verifiable-delay-function/main.py +++ /dev/null @@ -1,42 +0,0 @@ -import time -from src.verifiable_delay_function.verifiable_delay_function import VerifiableDelayFunction -from src.database.entity.verifiable_delay_function_entity import VerifiableDelayFunctionEntity -from src.converters.verifiable_delay_function_converter import conver_verifiable_delay_function_to_entity -from src.protocol_constants import BIT_SIZE, TOTAL_SQUARINGS, NUM_SEGMENTS - -def main(): - """ - Initializes a VDF with protocol constants. Generates a proof by performing sequential squarings in - the RSA group, then verifies the proof with parallel verification. Finally, converts the VDF to a - database entity and saves it. - """ - # Initialize VerifiableDelayFunction with protocol constants - vdf = VerifiableDelayFunction(bit_size=BIT_SIZE, T=TOTAL_SQUARINGS, num_segments=NUM_SEGMENTS) - print("Generated RSA modulus N:", vdf.N) - - # Time the proof generation - start_time = time.time() - y, proof = vdf.generate_proof() - generation_time = time.time() - start_time - print("VDF output (y):", y) - print(f"Proof generation time: {generation_time:.4f} seconds") - - # # Time the parallel verification - # start_time = time.time() - # is_valid_parallel = vdf.parallel_verify(y, proof) - # parallel_verification_time = time.time() - start_time - # print("Parallel verification:", "Valid" if is_valid_parallel else "Invalid") - # print(f"Parallel verification time: {parallel_verification_time:.4f} seconds") - - # if not is_valid_parallel: - # print("Verification failed. Aborting save.") - # return - - # Convert the puzzle instance to a VerifiableDelayFunctionEntity for database storage - entity: VerifiableDelayFunctionEntity = conver_verifiable_delay_function_to_entity(vdf) - - # Save the entity to the database - entity.save() - -if __name__ == "__main__": - main() diff --git a/verifiable-delay-function/src/converters/verifiable_delay_function_converter.py b/verifiable-delay-function/src/converters/verifiable_delay_function_converter.py deleted file mode 100644 index 399eba6..0000000 --- a/verifiable-delay-function/src/converters/verifiable_delay_function_converter.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List -from gmpy2 import mpz -from src.database.entity.verifiable_delay_function_entity import VerifiableDelayFunctionEntity -from src.verifiable_delay_function.verifiable_delay_function import VerifiableDelayFunction - -def conver_verifiable_delay_function_to_entity(verifiable_delay_function: VerifiableDelayFunction) -> VerifiableDelayFunctionEntity: - """ - Converts a completed VerifiableDelayFunction instance into a VerifiableDelayFunctionEntity instance for database storage. - - Args: - vdf_instance (VerifiableDelayFunction): The completed VDF instance to convert. - - Returns: - VerifiableDelayFunctionEntity: A new VerifiableDelayFunctionEntity instance populated with data from the VDF. - """ - # Ensure that `y` and `proof` are available - if not hasattr(verifiable_delay_function, 'y') or not verifiable_delay_function.proof: - raise ValueError("The VDF instance must be evaluated and proofed before conversion.") - - # Convert the modulus, input, and output to hex strings - modulus_hex: str = verifiable_delay_function.N.digits(16) - input_hex: str = verifiable_delay_function.x.digits(16) - output_hex: str = verifiable_delay_function.y.digits(16) - - # Convert proof list to JSON-serializable format by encoding each segment as hex - proof_json: List[str] = [checkpoint.digits(16) for checkpoint in verifiable_delay_function.proof] - - # Create and return a new VerifiableDelayFunctionEntity instance - return VerifiableDelayFunctionEntity( - modulus_hex=modulus_hex, - input_hex=input_hex, - output_hex=output_hex, - proof=proof_json - ) diff --git a/verifiable-delay-function/src/database/entity/__init__.py b/verifiable-delay-function/src/database/entity/__init__.py deleted file mode 100644 index 6eb1c9b..0000000 --- a/verifiable-delay-function/src/database/entity/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Database entity models.""" - -from .verifiable_delay_function_entity import VerifiableDelayFunctionEntity -from .TimeLockPuzzleEntity import TimeLockPuzzleEntity -from .RSAEntity import RSAEntity - -__all__ = ["VerifiableDelayFunctionEntity", "TimeLockPuzzleEntity", "RSAEntity"] diff --git a/verifiable-delay-function/src/database/entity/verifiable_delay_function_entity.py b/verifiable-delay-function/src/database/entity/verifiable_delay_function_entity.py deleted file mode 100644 index bedda4d..0000000 --- a/verifiable-delay-function/src/database/entity/verifiable_delay_function_entity.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import List -import uuid -from sqlalchemy import Column, String, LargeBinary, JSON, Integer -from sqlalchemy.ext.declarative import declarative_base - -from src.database.mixins.saveable import Saveable -from src.database.database import get_orm_base - -# Define the Base class for ORM models -Base = get_orm_base() - -class VerifiableDelayFunctionEntity(Base, Saveable): - __tablename__ = 'verifiable_delay_functions' - - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) # Unique generated string ID - request_id = Column(String, nullable=True) # Nullable request_id to be filled later - modulus = Column(String, nullable=False) # Store hex string of modulus - input = Column(String, nullable=False) # Store hex string of input - output = Column(String, nullable=False) # Store hex string of output - proof = Column(JSON, nullable=False) # Proof as a JSON list of hex strings - - def __repr__(self): - return (f" mpz: - """Optimized evaluation using batched exponentiation to reduce calls to powmod.""" - result = mpz(self.x) - self.proof = [] - - # Instead of squaring `segment_length` times, use exponentiation - segment_exp = 2**self.segment_length - - # Iterate over each segment and apply the batched exponentiation - for segment in range(self.num_segments): - result = powmod( - result, segment_exp, self.N - ) # Exponentiate by 2^segment_length in a single step - self.proof.append( - mpz(result) - ) # Store the intermediate result as part of the proof - - self.y = result - return result - - def generate_proof(self) -> Tuple[mpz, List[mpz]]: - y = self.evaluate() - return y, self.proof - - @staticmethod - def verify_segment(args: Tuple[mpz, mpz, int, mpz]) -> bool: - """ - Verify a segment by performing segment_length squarings. - - Args: - args (Tuple): A tuple containing the start_value, expected end_value, - segment_length, and modulus N. - - Returns: - bool: True if the computed end_value matches the expected end_value, False otherwise. - """ - start_value, end_value, segment_length, N = args - result = mpz(start_value) - for _ in range(segment_length): - result = powmod(result, 2, N) - return result == end_value - - def parallel_verify(self, y: mpz, proof: List[mpz]) -> bool: - """ - Performs parallel verification by checking each proof segment concurrently - using multiprocessing for true parallelism. - - Args: - y (mpz): The final VDF result to verify. - proof (List[mpz]): A list of intermediate values for parallel verification. - - Returns: - bool: True if verification succeeds, False otherwise. - """ - # Step 1: Prepare arguments for each segment verification - tasks = [ - (proof[i - 1] if i > 0 else self.x, proof[i], self.segment_length, self.N) - for i in range(len(proof)) - ] - - # Step 2: Use multiprocessing Pool to verify each segment in parallel - with Pool() as pool: - results = pool.map(self.verify_segment, tasks) - - # Step 3: Check if all segments verified successfully - if not all(results): - print("Parallel verification failed.") - return False - - # Final check: Verify that the last computed segment result matches y - return proof[-1] == y - - ##Private## - def _generate_rsa_modulus(self) -> Tuple[mpz, mpz, mpz]: - while True: - # Generate p and q with slightly fewer bits - p = gmpy2.next_prime(mpz_urandomb(self.rand, self.bit_size // 2 - 1)) - q = gmpy2.next_prime(mpz_urandomb(self.rand, self.bit_size // 2 - 1)) - - N = p * q - - # Check if N is within the desired bit size - if N.bit_length() <= self.bit_size: - return N, p, q - - def _generate_random_challenge(self) -> mpz: - return mpz_urandomb(self.rand, self.bit_size // 2) From e6d9b831c87733822b58a0914c1c4a827392db2c Mon Sep 17 00:00:00 2001 From: K <111819113+KennySwayzee93@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:33:04 -0400 Subject: [PATCH 36/80] remove 0x --- puzzle-generator/src/converters/rsa_converter.py | 8 ++++---- .../src/converters/time_lock_puzzle_converter.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/puzzle-generator/src/converters/rsa_converter.py b/puzzle-generator/src/converters/rsa_converter.py index 8c5c1c3..209308c 100644 --- a/puzzle-generator/src/converters/rsa_converter.py +++ b/puzzle-generator/src/converters/rsa_converter.py @@ -18,8 +18,8 @@ def to_entity(rsa: RSA) -> RSAEntity: RSAEntity: The database entity """ return RSAEntity( - hex(rsa.get_p()), - hex(rsa.get_q()), - hex(rsa.get_N()), - hex(rsa.get_phi()), + hex(rsa.get_p())[2:], # remove 0x + hex(rsa.get_q())[2:], # remove 0x + hex(rsa.get_N())[2:], # remove 0x + hex(rsa.get_phi())[2:], # remove 0x ) diff --git a/puzzle-generator/src/converters/time_lock_puzzle_converter.py b/puzzle-generator/src/converters/time_lock_puzzle_converter.py index 7b789b5..4eb498d 100644 --- a/puzzle-generator/src/converters/time_lock_puzzle_converter.py +++ b/puzzle-generator/src/converters/time_lock_puzzle_converter.py @@ -21,9 +21,9 @@ def to_entity(puzzle: TimeLockPuzzle, rsa_id: str, y: MPZ) -> TimeLockPuzzleEnti TimeLockPuzzleEntity: The database entity """ return TimeLockPuzzleEntity( - x_hex=hex(puzzle.get_x()), - y_hex=hex(y), - t=str(puzzle.get_t()), - N_hex=hex(puzzle.get_N()), + x_hex=hex(puzzle.get_x())[2:], # remove 0x + y_hex=hex(y)[2:], # remove 0x + t=str(puzzle.get_t()), # remove 0x + N_hex=hex(puzzle.get_N())[2:], # remove 0x rsa_id=rsa_id, ) From 1a268d17ab0e39a819cd7ef600e04a9ea447a1aa Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 28 Mar 2025 12:47:14 -0400 Subject: [PATCH 37/80] updated --- docker-compose/mass-dev-docker-compose.yml | 32 ++++++------ orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 59 +++++++++------------- requester/docs/development.md | 2 +- requester/package.json | 2 +- requester/src/app.ts | 50 +++++++++++++++--- 7 files changed, 88 insertions(+), 61 deletions(-) diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml index 5a689fc..670d5a2 100644 --- a/docker-compose/mass-dev-docker-compose.yml +++ b/docker-compose/mass-dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres3: condition: service_healthy @@ -138,7 +138,7 @@ services: retries: 5 orchestrator4: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres4: condition: service_healthy @@ -177,7 +177,7 @@ services: retries: 5 orchestrator5: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres5: condition: service_healthy @@ -216,7 +216,7 @@ services: retries: 5 orchestrator6: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres6: condition: service_healthy @@ -255,7 +255,7 @@ services: retries: 5 orchestrator7: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres7: condition: service_healthy @@ -294,7 +294,7 @@ services: retries: 5 orchestrator8: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres8: condition: service_healthy @@ -333,7 +333,7 @@ services: retries: 5 orchestrator9: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres9: condition: service_healthy @@ -372,7 +372,7 @@ services: retries: 5 orchestrator10: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres10: condition: service_healthy @@ -411,7 +411,7 @@ services: retries: 5 orchestrator11: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres11: condition: service_healthy @@ -450,7 +450,7 @@ services: retries: 5 orchestrator12: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres12: condition: service_healthy @@ -489,7 +489,7 @@ services: retries: 5 orchestrator13: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres13: condition: service_healthy @@ -528,7 +528,7 @@ services: retries: 5 orchestrator14: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres14: condition: service_healthy @@ -567,7 +567,7 @@ services: retries: 5 orchestrator15: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.92 depends_on: postgres15: condition: service_healthy @@ -601,7 +601,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.3.1 + image: randao/requester:v0.3.77 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 83c8940..59a8612 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.3.45 # You can change this value to any version you want +export VERSION=v0.3.85 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index e8f08fa..8421293 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "5.3.21", + "ao-process-clients": "5.4.9", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index e06bbc4..812c56c 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -6,53 +6,44 @@ import { dbConfig } from './db_config.js'; import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; import Arweave from 'arweave'; -// const RANDOM_CONFIG: RandomClientConfig = { -// wallet: JSON.parse(process.env.WALLET_JSON!), -// tokenProcessId: '', -// processId: '' -// } -//const randclient: IRandomClient = RandomClient.autoConfiguration() +const AO_CONFIG_BASE = { + MU_URL: "https://ur-mu.randao.net", + //MU_URL: "https://mu.ao-testnet.xyz", + GATEWAY_URL: "https://arweave.net", +}; + +const CU_URLS = [ + "https://ur-cu.randao.net", +]; + +// const CU_URLS = [ +// "https://cu.ao-testnet.xyz" +// ]; let randomClientInstance: RandomClient | null = null; async function getRandomClient(): Promise { + const randomIndex = Math.floor(Math.random() * CU_URLS.length); // Pick a random index + const AO_CONFIG = { + ...AO_CONFIG_BASE, + CU_URL: CU_URLS[randomIndex], // Select a random CU URL + }; + + console.log(`Using CU_URL: ${AO_CONFIG.CU_URL}`); // Log the selected CU URL + if (!randomClientInstance) { - const RANDOM_CONFIG: RandomClientConfig = await new RandomClientConfigBuilder() + randomClientInstance = ((await RandomClient.defaultBuilder()) + .withAOConfig(AO_CONFIG)) + .withProcessId("BPafv2apbvSU0SRZEksMULFtKQQb0KvS7PBTPadFVSQ") .withWallet(JSON.parse(process.env.WALLET_JSON!)) .build(); - - randomClientInstance = new RandomClient(RANDOM_CONFIG); } - return randomClientInstance; } -// async function getRandomClient(): Promise { -// if (!randomClientInstance) { -// const RANDOM_CONFIG: BaseClientConfigBuilder = await new BaseClientConfigBuilder() -// .withWallet(JSON.parse(process.env.WALLET_JSON!)) -// .withAOConfig({ -// CU_URL: "https://cu.randao.net", -// MODE: 'legacy' -// }) -// .build(); - -// randomClientInstance = new RandomClient(RANDOM_CONFIG); -// } - -// return randomClientInstance; -// } - -// async function getStakingClient(): Promise{ -// let test = await getProviderStakingClientAutoConfiguration() -// test.wallet = JSON.parse(process.env.WALLET_JSON!) -// const randclient = new ProviderStakingClient(test) -// return randclient -// } - const docker = new Docker(); // Constants for configuration -const POLLING_INTERVAL_MS = 1000; +const POLLING_INTERVAL_MS = 10000; const MINIMUM_ENTRIES = 1000; const DRYRUNTIMEOUT = 30000; // 30 seconds const MAX_OUTSTANDING_VDF_CONTAINERS = 10; diff --git a/requester/docs/development.md b/requester/docs/development.md index 5cb89cb..08e7c63 100644 --- a/requester/docs/development.md +++ b/requester/docs/development.md @@ -2,4 +2,4 @@ To build: Save all files Run: -docker build -t randao/requester:latest -t randao/requester:v0.1.5 . \ No newline at end of file +docker build -t randao/requester:latest -t randao/requester:v0.3.55 . \ No newline at end of file diff --git a/requester/package.json b/requester/package.json index 59e9853..982aeb7 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "5.3.21", + "ao-process-clients": "5.4.9", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index 2e369cb..5bd7c8f 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -9,7 +9,7 @@ import { RandomClientConfigBuilder, } from "ao-process-clients"; -const RETRY_DELAY_MS = 10000; // 10 seconds +const RETRY_DELAY_MS = 60000; // 60 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; @@ -17,14 +17,48 @@ const CHANCE_TO_CALL_RANDOM = 1; let cachedProviders: string[] = []; let lastProviderRefresh = 0; -async function getRandomClient(): Promise{ - // let test = await getRandomClientAutoConfiguration() - // test.wallet = JSON.parse(process.env.WALLET_JSON!) +const AO_CONFIG_BASE = { + MU_URL: "https://ur-mu.randao.net", + // MU_URL: "https://mu.ao-testnet.xyz", + GATEWAY_URL: "https://arweave.net", +}; + +const CU_URLS = [ + "https://ur-cu.randao.net", + //"https://cu2.randao.net", + //"https://cu3.randao.net", + //"https://cu4.randao.net", + //"https://cu5.randao.net", + //"https://cu6.randao.net:444" +]; + +// const CU_URLS = [ +// "https://cu.ao-testnet.xyz" +// ]; + +let randomClientInstance: RandomClient | null = null; + +async function getRandomClient(): Promise { + const randomIndex = Math.floor(Math.random() * CU_URLS.length); // Pick a random index + const AO_CONFIG = { + ...AO_CONFIG_BASE, + CU_URL: CU_URLS[randomIndex], // Select a random CU URL + }; + + console.log(`Using CU_URL: ${AO_CONFIG.CU_URL}`); // Log the selected CU URL - const RANDOM_CONFIG: RandomClientConfig = await new RandomClientConfigBuilder().withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)).build() - const randclient = new RandomClient(RANDOM_CONFIG) - return randclient + if (!randomClientInstance) { + randomClientInstance = ((await RandomClient.defaultBuilder()) + .withAOConfig(AO_CONFIG)) + .withProcessId("BPafv2apbvSU0SRZEksMULFtKQQb0KvS7PBTPadFVSQ") + .withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)) + .build(); } + return randomClientInstance; +} + + + let totalRandomCalled = 0; let totalTimeToFulfill = 0; let fulfilledRequests = 0; @@ -54,6 +88,7 @@ async function getRandomProviders(randclient: RandomClient): Promise<{ providers // Create the actual provider fetch promise const fetchPromise = async () => { const providerInfo = await randclient.getAllProviderActivity(); + console.log(providerInfo) const eligibleProviders = providerInfo //@ts-ignore .filter(provider => provider.active === 1) @@ -115,6 +150,7 @@ async function main() { const { providers, count } = await getRandomProviders(randclient); console.log(`Selected ${count} providers:`, providers); await randclient.createRequest(providers, count, callbackId); + //await randclient.createRequest(["X1tqliRkKnClhVQ4aIeyuOaPTzr5PfnxqAoSdpTzZy8"], 1, "123"); totalRandomCalled++; console.log("Random request initiated. Awaiting request ID in open requests..."); } From b0b39e7fa1b2bf556aab94864109072c539f78fe Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 28 Mar 2025 12:48:22 -0400 Subject: [PATCH 38/80] updated --- docker-compose/dev-docker-compose.yml | 6 ++--- docker-compose/docker-compose.yml | 2 +- docker-compose/mass-dev-docker-compose.yml | 30 +++++++++++----------- terraform/ecs.tf | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml index 4833642..8a6f997 100644 --- a/docker-compose/dev-docker-compose.yml +++ b/docker-compose/dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.99 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.99 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.99 depends_on: postgres3: condition: service_healthy diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index ae00b9e..e08beb0 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.3.45 + image: randao/orchestrator:v0.3.99 depends_on: postgres: condition: service_healthy diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml index 670d5a2..c8acec0 100644 --- a/docker-compose/mass-dev-docker-compose.yml +++ b/docker-compose/mass-dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres3: condition: service_healthy @@ -138,7 +138,7 @@ services: retries: 5 orchestrator4: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres4: condition: service_healthy @@ -177,7 +177,7 @@ services: retries: 5 orchestrator5: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres5: condition: service_healthy @@ -216,7 +216,7 @@ services: retries: 5 orchestrator6: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres6: condition: service_healthy @@ -255,7 +255,7 @@ services: retries: 5 orchestrator7: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres7: condition: service_healthy @@ -294,7 +294,7 @@ services: retries: 5 orchestrator8: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres8: condition: service_healthy @@ -333,7 +333,7 @@ services: retries: 5 orchestrator9: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres9: condition: service_healthy @@ -372,7 +372,7 @@ services: retries: 5 orchestrator10: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres10: condition: service_healthy @@ -411,7 +411,7 @@ services: retries: 5 orchestrator11: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres11: condition: service_healthy @@ -450,7 +450,7 @@ services: retries: 5 orchestrator12: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres12: condition: service_healthy @@ -489,7 +489,7 @@ services: retries: 5 orchestrator13: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres13: condition: service_healthy @@ -528,7 +528,7 @@ services: retries: 5 orchestrator14: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres14: condition: service_healthy @@ -567,7 +567,7 @@ services: retries: 5 orchestrator15: - image: randao/orchestrator:v0.3.92 + image: randao/orchestrator:v0.3.99 depends_on: postgres15: condition: service_healthy diff --git a/terraform/ecs.tf b/terraform/ecs.tf index ccd5cee..e89e550 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -62,7 +62,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([ { name = "orchestrator" - image = "randao/orchestrator:v0.3.45" + image = "randao/orchestrator:v0.3.99" environment = [ { name = "ENVIRONMENT" From 40d7169e30875d2b5537ee5588232ca4aaf7e386 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 28 Mar 2025 18:04:39 -0400 Subject: [PATCH 39/80] done --- docker-compose/dev-docker-compose.yml | 6 +- docker-compose/docker-compose.yml | 2 +- docker-compose/mass-dev-docker-compose.yml | 34 +-- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 287 ++++++++++-------- orchestrator/src/ecs_config.ts | 2 +- puzzle-generator/Dockerfile | 2 +- puzzle-generator/docs/developing.md | 17 +- .../src/database/entity/RSAEntity.py | 4 +- .../database/entity/TimeLockPuzzleEntity.py | 4 +- requester/docs/development.md | 2 +- requester/package.json | 2 +- requester/src/app.ts | 28 +- terraform/ecs.tf | 4 +- 15 files changed, 211 insertions(+), 187 deletions(-) diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml index 8a6f997..2b15562 100644 --- a/docker-compose/dev-docker-compose.yml +++ b/docker-compose/dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres3: condition: service_healthy diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index e08beb0..74fb28f 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres: condition: service_healthy diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml index c8acec0..c69da7e 100644 --- a/docker-compose/mass-dev-docker-compose.yml +++ b/docker-compose/mass-dev-docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: # Instance 1 postgres1: @@ -21,7 +19,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres1: condition: service_healthy @@ -60,7 +58,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres2: condition: service_healthy @@ -99,7 +97,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres3: condition: service_healthy @@ -138,7 +136,7 @@ services: retries: 5 orchestrator4: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres4: condition: service_healthy @@ -177,7 +175,7 @@ services: retries: 5 orchestrator5: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres5: condition: service_healthy @@ -216,7 +214,7 @@ services: retries: 5 orchestrator6: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres6: condition: service_healthy @@ -255,7 +253,7 @@ services: retries: 5 orchestrator7: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres7: condition: service_healthy @@ -294,7 +292,7 @@ services: retries: 5 orchestrator8: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres8: condition: service_healthy @@ -333,7 +331,7 @@ services: retries: 5 orchestrator9: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres9: condition: service_healthy @@ -372,7 +370,7 @@ services: retries: 5 orchestrator10: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres10: condition: service_healthy @@ -411,7 +409,7 @@ services: retries: 5 orchestrator11: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres11: condition: service_healthy @@ -450,7 +448,7 @@ services: retries: 5 orchestrator12: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres12: condition: service_healthy @@ -489,7 +487,7 @@ services: retries: 5 orchestrator13: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres13: condition: service_healthy @@ -528,7 +526,7 @@ services: retries: 5 orchestrator14: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres14: condition: service_healthy @@ -567,7 +565,7 @@ services: retries: 5 orchestrator15: - image: randao/orchestrator:v0.3.99 + image: randao/orchestrator:v0.4.2 depends_on: postgres15: condition: service_healthy @@ -601,7 +599,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.3.77 + image: randao/requester:v0.4.0 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 59a8612..82b5b5a 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.3.85 # You can change this value to any version you want +export VERSION=v0.4.2 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 8421293..2a04907 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "5.4.9", + "ao-process-clients": "^5.4.16", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 812c56c..78763ba 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -6,34 +6,19 @@ import { dbConfig } from './db_config.js'; import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; import Arweave from 'arweave'; -const AO_CONFIG_BASE = { +const AO_CONFIG = { MU_URL: "https://ur-mu.randao.net", - //MU_URL: "https://mu.ao-testnet.xyz", + CU_URL: "https://ur-cu.randao.net", GATEWAY_URL: "https://arweave.net", }; -const CU_URLS = [ - "https://ur-cu.randao.net", -]; - -// const CU_URLS = [ -// "https://cu.ao-testnet.xyz" -// ]; let randomClientInstance: RandomClient | null = null; async function getRandomClient(): Promise { - const randomIndex = Math.floor(Math.random() * CU_URLS.length); // Pick a random index - const AO_CONFIG = { - ...AO_CONFIG_BASE, - CU_URL: CU_URLS[randomIndex], // Select a random CU URL - }; - - console.log(`Using CU_URL: ${AO_CONFIG.CU_URL}`); // Log the selected CU URL - + if (!randomClientInstance) { randomClientInstance = ((await RandomClient.defaultBuilder()) .withAOConfig(AO_CONFIG)) - .withProcessId("BPafv2apbvSU0SRZEksMULFtKQQb0KvS7PBTPadFVSQ") .withWallet(JSON.parse(process.env.WALLET_JSON!)) .build(); } @@ -50,7 +35,7 @@ const MAX_OUTSTANDING_VDF_CONTAINERS = 10; const RANDOM_PER_VDF = 10; const MAX_RETRIES = 10; const RETRY_DELAY_MS = 10000; -const VDF_JOB_IMAGE = 'randao/vdf_job:v0.1.4'; +const VDF_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); const ongoingContainers = new Set(); // Track container IDs of running Docker containers @@ -107,27 +92,44 @@ async function connectWithRetry(): Promise { throw new Error("Failed to connect to PostgreSQL after multiple attempts"); } -// Setup the `verifiable_delay_functions` table if not exists +// Function to initialize the PostgreSQL database schema async function setupDatabase(client: Client): Promise { - await client.query(` - CREATE TABLE IF NOT EXISTS verifiable_delay_functions ( - id TEXT PRIMARY KEY, - request_id TEXT, - modulus TEXT NOT NULL, - input TEXT NOT NULL, - output TEXT NOT NULL, - proof JSON NOT NULL, - date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - detected_completed TIMESTAMP NULL - ); - `); + try { + // Drop old tables maybe + await client.query(` + DROP TABLE IF EXISTS verifiable_delay_functions CASCADE; + `); - // Ensure detected_completed column exists in case the table was created before it was added - await client.query(` - ALTER TABLE verifiable_delay_functions - ADD COLUMN IF NOT EXISTS detected_completed TIMESTAMP NULL; + // Create the rsa_keys table + await client.query(` + CREATE TABLE rsa_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + p TEXT NOT NULL, + q TEXT NOT NULL, + modulus TEXT NOT NULL UNIQUE, -- Store hex string of modulus N + phi TEXT NOT NULL + ); `); - console.log("Database setup complete. 'verifiable_delay_functions' table is ready."); + + // Create the time_lock_puzzles table with relation to rsa_keys based on rsa_id + await client.query(` +CREATE TABLE time_lock_puzzles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + x TEXT NOT NULL, + y TEXT NOT NULL, + t TEXT NOT NULL, + modulus TEXT NOT NULL, -- Store hex string of modulus N + request_id TEXT NULL, + rsa_id UUID NOT NULL UNIQUE, + detected_completed TIMESTAMP NULL, -- Added to track completion time + FOREIGN KEY (rsa_id) REFERENCES rsa_keys(id) ON DELETE CASCADE +); + `); + + console.log("✅ Database setup complete. Tables are properly linked on rsa_id."); + } catch (error) { + console.error("❌ Error setting up database:", error); + } } @@ -164,11 +166,11 @@ async function triggerVDFJobPod(): Promise { } else { // Check if we have reached the maximum number of containers if (ongoingContainers.size >= MAX_OUTSTANDING_VDF_CONTAINERS) { - console.log(`Maximum outstanding VDF containers (${MAX_OUTSTANDING_VDF_CONTAINERS}) reached. Not starting new container.`); + console.log(`Maximum outstanding puzzle-gen containers (${MAX_OUTSTANDING_VDF_CONTAINERS}) reached. Not starting new container.`); return null; } - const containerName = `vdf_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; console.log(`Starting Docker container with name: ${containerName}`); try { if (!pulledDockerimage) { @@ -188,7 +190,7 @@ async function triggerVDFJobPod(): Promise { } const container = await docker.createContainer({ Image: VDF_JOB_IMAGE, - Cmd: ['sh', '-c', `for i in $(seq 1 ${RANDOM_PER_VDF}); do python main.py; done`], + Cmd: ['sh', '-c', `python3 main.py ${RANDOM_PER_VDF}`], Env: [ `DATABASE_TYPE=postgresql`, `DATABASE_HOST=${dbConfig.host}`, @@ -293,7 +295,7 @@ function isDockerError(error: unknown): error is { statusCode: number } { // Function to process hex output for 64-bit modulus function hexMod64Bit(expectedOutput: string): { expectedOutput64BitBase10: string } { // Parse the hexadecimal string into a BigInt - const number = BigInt(`0x${expectedOutput}`); + const number = BigInt(`${expectedOutput}`); // Define the 64-bit modulus (2^64 - 1) const modulus = BigInt("0x7FFFFFFFF"); @@ -310,11 +312,6 @@ function hexMod64Bit(expectedOutput: string): { expectedOutput64BitBase10: strin }; } -// Function to prepend 0x to hex strings -function addHexPrefix(value: string): string { - return value.startsWith('0x') ? value : `0x${value}`; -} - function updateAvailableValuesAsync(currentCount: number) { return (async () => { try { @@ -383,7 +380,7 @@ async function checkAndFetchIfNeeded(client: Client) { const on_chain_avalible_random = await getProviderAvailableRandomValues(PROVIDER_ID); // Query current count of usable DB entries const res = await client.query( - 'SELECT COUNT(*) AS count FROM verifiable_delay_functions WHERE request_id IS NULL' + 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' ); const currentCount = parseInt(res.rows[0].count, 10); console.log("Total usable DB entries: " + currentCount); @@ -439,8 +436,8 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL try { // Fetch the necessary details from the database using requestId const res = await client.query( - `SELECT id, modulus, input - FROM verifiable_delay_functions + `SELECT id, modulus, x + FROM time_lock_puzzles WHERE request_id = $1`, [requestId] ); @@ -450,16 +447,18 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL return; } - const { id: dbId, modulus, input } = res.rows[0]; + const { id: dbId, modulus, x: input } = res.rows[0]; console.log(`${parentLogId} Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); - // Add hex prefix to modulus and input - const hexModulus = addHexPrefix(modulus); - const hexInput = addHexPrefix(input); - console.log(`${parentLogId} Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); - await (await getRandomClient()).postVDFChallenge(requestId, hexModulus, hexInput); + await (await getRandomClient()).commit({ + requestId: requestId, + puzzle: { + input: input, + modulus: modulus + } + }); console.log(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); } catch (error) { console.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); @@ -472,27 +471,39 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { try { // Fetch the output and proof from the database using the requestId - const res = await client.query('SELECT id, output, proof FROM verifiable_delay_functions WHERE request_id = $1', [requestId]); + const res = await client.query( + `SELECT + tlp.id, + tlp.y AS output, + rk.p, + rk.q + FROM time_lock_puzzles tlp + JOIN rsa_keys rk ON tlp.rsa_id = rk.id + WHERE tlp.request_id = $1`, + [requestId] + ); + if (!res.rowCount) { console.error(`No entry found for request ID: ${requestId}`); return; } - const { id: dbId, output, proof } = res.rows[0]; - - console.log(`${parentLogId} Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}`); - - // Process the output through hexMod64Bit - const processedOutput = hexMod64Bit(output).expectedOutput64BitBase10; - console.log(`${parentLogId} Processed output: ${processedOutput} For request ID: ${requestId}`); - // Process the proof array - add hex prefix to each element - let processedProof = proof; - if (Array.isArray(proof)) { - processedProof = proof.map(element => addHexPrefix(element)); - } - const proofString = JSON.stringify(processedProof); + // Map the response to structured variables +const { + id: dbId, + output, // Mapping 'y' to 'output' + p: rsaP, + q: rsaQ +} = res.rows[0]; + console.log(`${parentLogId} Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}, output: ${output}, rsaP: ${rsaP}, rsaQ ${rsaQ} `); console.log(`${parentLogId} Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); - await (await getRandomClient()).postVDFOutputAndProof(requestId, processedOutput, proofString); + await (await getRandomClient()).reveal({ + requestId: requestId, + rsa_key: { + p: rsaP, + q: rsaQ + } + }) console.log(`${parentLogId} Proof posted for request ID: ${requestId}`); } catch (error) { console.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); @@ -631,7 +642,9 @@ async function polling(client: any) { (async () => { const s4 = Date.now(); console.log(`${logId} Step 4 started.`); - await cleanupFulfilledEntries(client, openRequests, logId); + + //TODO enable this again later + //await cleanupFulfilledEntries(client, openRequests, logId); stepTracking.step4 = { completed: true, timeTaken: Date.now() - s4 }; console.log(`${logId} Step 4 completed. Time taken: ${stepTracking.step4.timeTaken}ms`); })(), @@ -667,77 +680,87 @@ async function processChallengeRequests( try { await client.query('BEGIN'); // Start transaction - + console.log(`${parentLogId} Fetching existing request mappings.`); + // Fetch already assigned request_id -> dbId mappings const existingMappingsRes = await client.query( - `SELECT request_id FROM verifiable_delay_functions + `SELECT request_id FROM time_lock_puzzles WHERE request_id = ANY($1) FOR UPDATE SKIP LOCKED`, [requestIds] ); - + const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); console.log(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); - + // Find only the unmapped requests (requestIds not in existingRequestIds) const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); console.log(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); - - // Fetch available DB entries for unmapped requests - console.log(`${parentLogId} Fetching available DB entries.`); - const dbRes = await client.query( - `SELECT id FROM verifiable_delay_functions - WHERE request_id IS NULL - ORDER BY id ASC - LIMIT $1 - FOR UPDATE SKIP LOCKED`, - [unmappedRequestIds.length] - ); - - const availableDbEntries = dbRes.rows.map(row => row.id); - console.log(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); - - // Reduce request list if we don’t have enough DB entries - if (availableDbEntries.length < unmappedRequestIds.length) { - console.log(`${parentLogId} Limiting requests to ${availableDbEntries.length} due to DB availability.`); - unmappedRequestIds.length = availableDbEntries.length; + + let mappedEntries: { requestId: string, dbId: number }[] = []; + + if (unmappedRequestIds.length > 0) { + console.log(`${parentLogId} Fetching available DB entries.`); + const dbRes = await client.query( + `SELECT id FROM time_lock_puzzles + WHERE request_id IS NULL + ORDER BY id ASC + LIMIT $1 + FOR UPDATE SKIP LOCKED`, + [unmappedRequestIds.length] + ); + + const availableDbEntries = dbRes.rows.map(row => row.id); + console.log(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); + + if (availableDbEntries.length > 0) { + const numMappings = Math.min(unmappedRequestIds.length, availableDbEntries.length); + + for (let i = 0; i < numMappings; i++) { + await client.query( + `UPDATE time_lock_puzzles + SET request_id = $1 + WHERE id = $2`, + [unmappedRequestIds[i], availableDbEntries[i]] + ); + mappedEntries.push({ requestId: unmappedRequestIds[i], dbId: availableDbEntries[i] }); + console.log(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); + } + } else { + console.log(`${parentLogId} No available DB entries for unmapped requests.`); + } } - - if (availableDbEntries.length === 0 && existingRequestIds.size === 0) { - console.log(`${parentLogId} No available DB entries to process and no existing mappings.`); - await client.query('COMMIT'); // Commit to release locks + + // Collect all request IDs (previously mapped + newly mapped) + const allRequestIds = [...existingRequestIds, ...mappedEntries.map(entry => entry.requestId)]; + + if (allRequestIds.length === 0) { + console.log(`${parentLogId} No requests to process. Committing transaction.`); + await client.query('COMMIT'); return; } - - // Map unmapped requestIds to available DB entries (1:1) - for (let i = 0; i < unmappedRequestIds.length; i++) { - await client.query( - `UPDATE verifiable_delay_functions - SET request_id = $1 - WHERE id = $2`, - [unmappedRequestIds[i], availableDbEntries[i]] - ); - console.log(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); - } - + await client.query('COMMIT'); // Commit all updates at once - - // Call fulfillRandomChallenge for all request IDs (existing + newly mapped) - // Create an array of promises and use Promise.all to await them all in parallel - const promises = requestIds.map(requestId => - fulfillRandomChallenge(client, requestId, parentLogId) - .catch(error => console.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) + console.log(`${parentLogId} Committed all changes. Now fulfilling challenges.`); + + // Call fulfillRandomChallenge for all request IDs + await Promise.all( + allRequestIds.map(requestId => + fulfillRandomChallenge(client, requestId, parentLogId) + .catch(error => console.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) + ) ); - - await Promise.all(promises); // Wait for all promises to resolve + console.log(`${parentLogId} All challenges fulfilled`); - - console.log(`${parentLogId} Step 2 completed.`); - } catch (error) { + } catch (error:any) { console.error(`${parentLogId} Error in processChallengeRequests:`, error); await client.query('ROLLBACK'); // Rollback on failure + + console.error(`SQL State: ${error.code}, Message: ${error.message}`); } + + } // Step 3: Process Output Requests (unchanged but with logging) @@ -782,7 +805,7 @@ async function cleanupFulfilledEntries( // Fetch all entries with a request_id const result = await client.query(` SELECT id, request_id, detected_completed - FROM verifiable_delay_functions + FROM time_lock_puzzles WHERE request_id IS NOT NULL `); @@ -810,20 +833,28 @@ async function cleanupFulfilledEntries( // Mark entries as completed if (markAsCompleted.length > 0) { await client.query(` - UPDATE verifiable_delay_functions + UPDATE time_lock_puzzles SET detected_completed = NOW() WHERE id = ANY($1) `, [markAsCompleted]); console.log(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); } - // Delete old completed entries + // Delete old completed entries //TODO make sure its cleaning up BOTH tables if (markForDeletion.length > 0) { await client.query(` - DELETE FROM verifiable_delay_functions - WHERE id = ANY($1) + DELETE FROM rsa_keys + WHERE id IN ( + SELECT rsa_id FROM time_lock_puzzles WHERE id = ANY($1) + ); + `, [markForDeletion]); + + await client.query(` + DELETE FROM time_lock_puzzles + WHERE id = ANY($1); `, [markForDeletion]); - console.log(`${parentLogId} Deleted ${markForDeletion.length} old completed entries.`); + + console.log(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); } await client.query('COMMIT'); @@ -849,7 +880,7 @@ async function run(): Promise { }); setInterval(async () => { - const res = await client.query('SELECT COUNT(*) as count FROM verifiable_delay_functions'); + const res = await client.query('SELECT COUNT(*) as count FROM time_lock_puzzles'); console.log(`Periodic log - Current database size: ${res.rows[0].count}`); // Check and fetch entries for the database if needed console.log("Step 0: Checking and fetching database entries if below threshold."); diff --git a/orchestrator/src/ecs_config.ts b/orchestrator/src/ecs_config.ts index e97184a..5bd863e 100644 --- a/orchestrator/src/ecs_config.ts +++ b/orchestrator/src/ecs_config.ts @@ -60,7 +60,7 @@ export async function launchVDFTask( containerOverrides: [ { name: 'vdf_job_container', // Must match the container name in the task definition - command: ['sh', '-c', `for i in $(seq 1 ${random_per_vdf}); do python main.py; done`], + command: ['sh', '-c', `python3 main.py ${random_per_vdf}`], }, ], }, diff --git a/puzzle-generator/Dockerfile b/puzzle-generator/Dockerfile index 9a05905..be0ffdf 100644 --- a/puzzle-generator/Dockerfile +++ b/puzzle-generator/Dockerfile @@ -1,5 +1,5 @@ # Use an official Python image as a base -FROM python:3.8-slim +FROM python:3.12 # Install system dependencies needed for gmpy2 and PostgreSQL connection RUN apt-get update && apt-get install -y \ diff --git a/puzzle-generator/docs/developing.md b/puzzle-generator/docs/developing.md index 6cd1ee3..d15c15f 100644 --- a/puzzle-generator/docs/developing.md +++ b/puzzle-generator/docs/developing.md @@ -52,4 +52,19 @@ pytest With coverage: ```bash pytest --cov=src -``` \ No newline at end of file +``` + + + + + + +# Build the Docker image with the version tag +docker build -t randao/puzzle-gen:latest -t randao/puzzle-gen:v0.1.1 . + +# Log in to Docker +docker login + +# Push the image with the version tag +docker push randao/puzzle-gen:latest +docker push randao/puzzle-gen:v0.1.1 \ No newline at end of file diff --git a/puzzle-generator/src/database/entity/RSAEntity.py b/puzzle-generator/src/database/entity/RSAEntity.py index b2e8b79..7ddb16f 100644 --- a/puzzle-generator/src/database/entity/RSAEntity.py +++ b/puzzle-generator/src/database/entity/RSAEntity.py @@ -18,7 +18,7 @@ class RSAEntity(Base, Saveable): id = Column(String, primary_key=True) # Unique generated string ID p = Column(String, nullable=False) # Store hex string of prime p q = Column(String, nullable=False) # Store hex string of prime q - N = Column(String, nullable=False) # Store hex string of modulus N + modulus = Column(String, nullable=False) # Store hex string of modulus N phi = Column(String, nullable=False) # Store hex string of Euler's totient puzzle = relationship( "TimeLockPuzzleEntity", back_populates="rsa", uselist=False @@ -39,5 +39,5 @@ def __init__(self, p_hex: str, q_hex: str, N_hex: str, phi_hex: str): self.id = str(uuid.uuid4()) # Generate ID on creation self.p = p_hex self.q = q_hex - self.N = N_hex + self.modulus = N_hex self.phi = phi_hex diff --git a/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py b/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py index 2abb780..487f5db 100644 --- a/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py +++ b/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py @@ -20,7 +20,7 @@ class TimeLockPuzzleEntity(Base, Saveable): x = Column(String, nullable=False) # Store hex string of input value x y = Column(String, nullable=False) # Store hex string of y value t = Column(String, nullable=False) # Store base 10 string of time parameter t - N = Column(String, nullable=False) # Store hex string of modulus N + modulus = Column(String, nullable=False) # Store hex string of modulus N request_id = Column( String, nullable=True ) # Optional associated randomness request id (will be filled within the provider node runtime) @@ -45,5 +45,5 @@ def __init__(self, x_hex: str, y_hex: str, t: str, N_hex: str, rsa_id: str): self.x = x_hex self.y = y_hex self.t = t - self.N = N_hex + self.modulus = N_hex self.rsa_id = rsa_id diff --git a/requester/docs/development.md b/requester/docs/development.md index 08e7c63..a275bc2 100644 --- a/requester/docs/development.md +++ b/requester/docs/development.md @@ -2,4 +2,4 @@ To build: Save all files Run: -docker build -t randao/requester:latest -t randao/requester:v0.3.55 . \ No newline at end of file +docker build -t randao/requester:latest -t randao/requester:v0.4.0 . \ No newline at end of file diff --git a/requester/package.json b/requester/package.json index 982aeb7..4d7aa9a 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "5.4.9", + "ao-process-clients": "^5.4.16", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index 5bd7c8f..74feb04 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -9,7 +9,7 @@ import { RandomClientConfigBuilder, } from "ao-process-clients"; -const RETRY_DELAY_MS = 60000; // 60 seconds +const RETRY_DELAY_MS = 2000; // 2 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; @@ -17,40 +17,20 @@ const CHANCE_TO_CALL_RANDOM = 1; let cachedProviders: string[] = []; let lastProviderRefresh = 0; -const AO_CONFIG_BASE = { +const AO_CONFIG = { MU_URL: "https://ur-mu.randao.net", - // MU_URL: "https://mu.ao-testnet.xyz", + Cu_URL: "https://ur-cu.randao.net", GATEWAY_URL: "https://arweave.net", }; -const CU_URLS = [ - "https://ur-cu.randao.net", - //"https://cu2.randao.net", - //"https://cu3.randao.net", - //"https://cu4.randao.net", - //"https://cu5.randao.net", - //"https://cu6.randao.net:444" -]; - -// const CU_URLS = [ -// "https://cu.ao-testnet.xyz" -// ]; - let randomClientInstance: RandomClient | null = null; async function getRandomClient(): Promise { - const randomIndex = Math.floor(Math.random() * CU_URLS.length); // Pick a random index - const AO_CONFIG = { - ...AO_CONFIG_BASE, - CU_URL: CU_URLS[randomIndex], // Select a random CU URL - }; - - console.log(`Using CU_URL: ${AO_CONFIG.CU_URL}`); // Log the selected CU URL if (!randomClientInstance) { randomClientInstance = ((await RandomClient.defaultBuilder()) .withAOConfig(AO_CONFIG)) - .withProcessId("BPafv2apbvSU0SRZEksMULFtKQQb0KvS7PBTPadFVSQ") + .withProcessId("2ExUldxQ5NA_hnElSWYq0_lCBgeQQPxPhFbWDFihDEY") .withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)) .build(); } diff --git a/terraform/ecs.tf b/terraform/ecs.tf index e89e550..b2c8d5a 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -62,7 +62,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([ { name = "orchestrator" - image = "randao/orchestrator:v0.3.99" + image = "randao/orchestrator:v0.4.2" environment = [ { name = "ENVIRONMENT" @@ -144,7 +144,7 @@ resource "aws_ecs_task_definition" "vdf_job" { container_definitions = jsonencode([ { name = "vdf_job_container" - image = "randao/vdf_job:v0.1.4" + image = "randao/puzzle-gen:v0.1.1" command = ["python", "main.py"], environment = [ { From 24b0f5b032b107ae725d386fe5b2684848a9a3a5 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 31 Mar 2025 11:04:09 -0400 Subject: [PATCH 40/80] newer random --- docker-compose/dev-docker-compose.yml | 6 +-- docker-compose/docker-compose.yml | 2 +- docker-compose/mass-dev-docker-compose.yml | 32 ++++++------ orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 59 ++++++++-------------- requester/src/app.ts | 2 +- terraform/ecs.tf | 2 +- 7 files changed, 44 insertions(+), 61 deletions(-) diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml index 2b15562..5b5d1ad 100644 --- a/docker-compose/dev-docker-compose.yml +++ b/docker-compose/dev-docker-compose.yml @@ -21,7 +21,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres1: condition: service_healthy @@ -60,7 +60,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres2: condition: service_healthy @@ -99,7 +99,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres3: condition: service_healthy diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 74fb28f..120c8a8 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres: condition: service_healthy diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml index c69da7e..f2c0528 100644 --- a/docker-compose/mass-dev-docker-compose.yml +++ b/docker-compose/mass-dev-docker-compose.yml @@ -19,7 +19,7 @@ services: retries: 5 orchestrator1: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres1: condition: service_healthy @@ -58,7 +58,7 @@ services: retries: 5 orchestrator2: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres2: condition: service_healthy @@ -97,7 +97,7 @@ services: retries: 5 orchestrator3: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres3: condition: service_healthy @@ -136,7 +136,7 @@ services: retries: 5 orchestrator4: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres4: condition: service_healthy @@ -175,7 +175,7 @@ services: retries: 5 orchestrator5: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres5: condition: service_healthy @@ -214,7 +214,7 @@ services: retries: 5 orchestrator6: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres6: condition: service_healthy @@ -253,7 +253,7 @@ services: retries: 5 orchestrator7: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres7: condition: service_healthy @@ -292,7 +292,7 @@ services: retries: 5 orchestrator8: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres8: condition: service_healthy @@ -331,7 +331,7 @@ services: retries: 5 orchestrator9: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres9: condition: service_healthy @@ -370,7 +370,7 @@ services: retries: 5 orchestrator10: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres10: condition: service_healthy @@ -409,7 +409,7 @@ services: retries: 5 orchestrator11: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres11: condition: service_healthy @@ -448,7 +448,7 @@ services: retries: 5 orchestrator12: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres12: condition: service_healthy @@ -487,7 +487,7 @@ services: retries: 5 orchestrator13: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres13: condition: service_healthy @@ -526,7 +526,7 @@ services: retries: 5 orchestrator14: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres14: condition: service_healthy @@ -565,7 +565,7 @@ services: retries: 5 orchestrator15: - image: randao/orchestrator:v0.4.2 + image: randao/orchestrator:v0.4.55 depends_on: postgres15: condition: service_healthy @@ -599,7 +599,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.4.0 + image: randao/requester:v0.4.2 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 82b5b5a..d7f3b1b 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.4.2 # You can change this value to any version you want +export VERSION=v0.4.55 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 78763ba..77f3ad5 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,7 +1,7 @@ import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; -import { BaseClientConfig, BaseClientConfigBuilder, GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RandomClientConfig, RandomClientConfigBuilder} from "ao-process-clients" +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient} from "ao-process-clients" import { dbConfig } from './db_config.js'; import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; import Arweave from 'arweave'; @@ -31,11 +31,9 @@ const docker = new Docker(); const POLLING_INTERVAL_MS = 10000; const MINIMUM_ENTRIES = 1000; const DRYRUNTIMEOUT = 30000; // 30 seconds -const MAX_OUTSTANDING_VDF_CONTAINERS = 10; -const RANDOM_PER_VDF = 10; +const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; const MAX_RETRIES = 10; const RETRY_DELAY_MS = 10000; -const VDF_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); const ongoingContainers = new Set(); // Track container IDs of running Docker containers @@ -133,7 +131,7 @@ CREATE TABLE time_lock_puzzles ( } -async function triggerVDFJobPod(): Promise { +async function triggerTimePuzzleJobPod(randomCount: number): Promise { if (ENVIRONMENT === 'cloud') { try { console.log("Cloud environment detected. Launching ECS task."); @@ -145,7 +143,7 @@ async function triggerVDFJobPod(): Promise { console.log("Network config:", cachedNetworkConfig); } - const taskArn = await launchVDFTask(ecs, cachedNetworkConfig, RANDOM_PER_VDF); + const taskArn = await launchVDFTask(ecs, cachedNetworkConfig, randomCount); if (taskArn) { ongoingContainers.add(taskArn); console.log(`ECS task started successfully: ${taskArn}`); @@ -164,19 +162,13 @@ async function triggerVDFJobPod(): Promise { return null; } } else { - // Check if we have reached the maximum number of containers - if (ongoingContainers.size >= MAX_OUTSTANDING_VDF_CONTAINERS) { - console.log(`Maximum outstanding puzzle-gen containers (${MAX_OUTSTANDING_VDF_CONTAINERS}) reached. Not starting new container.`); - return null; - } - const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; console.log(`Starting Docker container with name: ${containerName}`); try { if (!pulledDockerimage) { - console.log(`Pulling image: ${VDF_JOB_IMAGE}`); + console.log(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); await new Promise((resolve, reject) => { - docker.pull(VDF_JOB_IMAGE, (err: any, stream: NodeJS.ReadableStream) => { + docker.pull(TIME_PUZZLE_JOB_IMAGE, (err: any, stream: NodeJS.ReadableStream) => { if (err) { return reject(err); } @@ -189,8 +181,9 @@ async function triggerVDFJobPod(): Promise { pulledDockerimage = true; } const container = await docker.createContainer({ - Image: VDF_JOB_IMAGE, - Cmd: ['sh', '-c', `python3 main.py ${RANDOM_PER_VDF}`], + Image: TIME_PUZZLE_JOB_IMAGE, + Cmd: ['sh', '-c', `python3 main.py ${randomCount}`], + Env: [ `DATABASE_TYPE=postgresql`, `DATABASE_HOST=${dbConfig.host}`, @@ -339,40 +332,30 @@ async function shutdown() { async function getMoreRandom(currentCount: number) { const entriesNeeded = MINIMUM_ENTRIES - currentCount; - //TODO this math looked wrong in https://discord.com/channels/1209645894039896074/1333434937537204336/1336000074765045853 console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); ongoingRequest = true; - // Calculate how many containers to spawn - const possibleBatchCount = Math.ceil(entriesNeeded / RANDOM_PER_VDF); - const availableSpawns = Math.min( - possibleBatchCount, - MAX_OUTSTANDING_VDF_CONTAINERS - ongoingContainers.size - ); - - if (availableSpawns <= 0) { - console.log("Max outstanding containers reached. Skipping new container launches."); - ongoingRequest = false; - return; + if (ongoingContainers.size > 0) { + console.log("A puzzle-gen container is already running. Skipping new container launch."); + return null; } - console.log(`Spawning up to ${availableSpawns} containers to generate random values.`); + console.log(`Spawning a single container to generate ${entriesNeeded} random values.`); - for (let i = 0; i < availableSpawns; i++) { - try { - const jobId = await triggerVDFJobPod(); - if (jobId) { - console.log(`Job triggered: ${jobId}`); - ongoingContainers.add(jobId); - } - } catch (error) { - console.error('Error triggering job pod:', error); + try { + const jobId = await triggerTimePuzzleJobPod(entriesNeeded); + if (jobId) { + console.log(`Job triggered: ${jobId}`); + ongoingContainers.add(jobId); } + } catch (error) { + console.error('Error triggering job pod:', error); } } + // Function to check and fetch database entries as needed async function checkAndFetchIfNeeded(client: Client) { try { diff --git a/requester/src/app.ts b/requester/src/app.ts index 74feb04..e20b5ff 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -9,7 +9,7 @@ import { RandomClientConfigBuilder, } from "ao-process-clients"; -const RETRY_DELAY_MS = 2000; // 2 seconds +const RETRY_DELAY_MS = 10000; // 10 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; diff --git a/terraform/ecs.tf b/terraform/ecs.tf index b2c8d5a..ca10463 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -62,7 +62,7 @@ resource "aws_ecs_task_definition" "orchestrator_service" { container_definitions = jsonencode([ { name = "orchestrator" - image = "randao/orchestrator:v0.4.2" + image = "randao/orchestrator:v0.4.55" environment = [ { name = "ENVIRONMENT" From cd042b7ad4e74de067d2946be49c3080bae42a34 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 31 Mar 2025 12:56:44 -0400 Subject: [PATCH 41/80] V0.4.55 --- README.md | 96 +++++++++----- docker-compose/README.md | 90 ++++++++++--- orchestrator/src/app.ts | 14 +- terraform/README.md | 278 ++++++++++++++++++++------------------- 4 files changed, 291 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index 17e5b7f..209ef8c 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,30 @@ # Node Provider Setup Guide -This guide will walk you through getting your randomness provider set up and connected to the network so you can start contributing to the protocal and participating in decentralized randomness! +This guide will walk you through getting your randomness provider set up and connected to the network so you can start contributing to the protocol and participating in decentralized randomness! ## Table of Contents 1. [Introduction](#introduction) 2. [Hardware Requirements](#hardware-requirements) -3. [Deployment Options](#deployment-options) +3. [Randomness Generation](#randomness-generation) +4. [Deployment Options](#deployment-options) - [Option 1: AWS Deployment with Terraform](#option-1-aws-deployment-with-terraform) - - [Option 2: Virtual Machine Deployment with Docker Compose](#option-2-virtual-machine-deployment-with-docker-compose) -4. [Graceful Shutdown Policy](#graceful-shutdown-policy) + - [Option 2: Docker Compose Deployment](#option-2-docker-compose-deployment) +5. [Graceful Shutdown Policy](#graceful-shutdown-policy) +6. [Staking](#staking) --- ## Introduction -As a node provider, you are responsible for ensuring 100% uptime. In the event of downtime, it is mandatory to run the graceful shutdown script to prevent being slashed. +As a node provider, you are responsible for ensuring 100% uptime. In the event of necessary downtime, it is mandatory to run the graceful shutdown process to prevent being slashed. -Your provider does 3 main things. -1. It updates the amount of avalible random it has stored on chain. Each random takes a fair biot of compute to create so that it can be compliant to our commit reveal time delay scheme. -2. It detects someone has requested random from you and it provides the input number to your time delay function. This is not the final random number. -3. It detects all parties have submited their input number and then it provides the output number as well as the proof of history checkpoints to the chain to be verified. This output is the random number that will be used on chain. +Your provider performs 3 main functions: +1. It updates the amount of available random it has stored on chain. Each random value requires significant computation to create, ensuring compliance with our commit-reveal time delay scheme. +2. It detects when someone has requested random from you and provides the input number to your time delay function. This is not the final random number. +3. It detects when all parties have submitted their input numbers and then provides the output number along with the proof of history checkpoints to the chain for verification. This output is the random number that will be used on chain. -It will do these three steps as fast as it can inorder to get the complete random on chain as quickly as possible. Faster providers will be incentivised for their speed and slower ones will been penalized. If a provider is too slow for step 2 it will be slashed a small amount. If a provider is too slow for step 3 they will be considered malicious and slashed heavily. +These steps are executed as quickly as possible to get the complete random value on chain promptly. Faster providers will be incentivized for their speed, while slower ones will be penalized. If a provider is too slow for step 2, it will be slashed a small amount. If a provider is too slow for step 3, they will be considered malicious and slashed heavily. -In the event you need to take your provider offline you must run the gracefull shutdown which will run step 1 once with a value of -1 indicating you are no longer offering random. After that your node will finish all requests in step 2 and 3 then turn off. +If you need to take your provider offline, you must run the graceful shutdown process, which will execute step 1 once with a value of -1, indicating you are no longer offering random. After that, your node will finish all pending requests in steps 2 and 3 before shutting down. --- @@ -32,52 +34,86 @@ To run a node, the following hardware specifications are required: - **Minimum Hardware Requirements:** - 4 GB memory - 2 CPU cores -- **Recommended Deployment:** Access to an AWS account (we handle the configuration) - **Note:** These requirements will increase over time to meet network demands. --- +## Randomness Generation -## What the hardware runs -The hardware you stand up runs 3 services. It stands up a provider. A database for the provider and it spins up temporary jobs to generate random and store it in the database. -In AWS the cheapest and highest performance solution is used for each of these. when running with docekr compose your machinbe will run each of these services itself in a containerized environment. Quick and scalable but not as cheap or efficient as the AWS solution. +Our system now uses cryptographic time lock puzzles instead of Verifiable Delay Functions (VDF) for randomness generation. The provider "mines" for these puzzles, which creates a provable time delay between commitment and revelation of random values. This approach enhances security while maintaining verifiability of the randomness generated. +The time lock puzzles require significant initial computation but become less resource-intensive once you've mined and stored a sufficient number of them for sale. This makes the provider more efficient over time as your puzzle inventory grows. + +--- + +## What the Hardware Runs +The hardware you set up runs 3 key services: +1. A provider service +2. A database for the provider +3. Temporary jobs to generate random values and store them in the database + +Both of our deployment options are designed to run these services efficiently. + +--- ## Deployment Options +We now fully support and recommend two deployment methods, depending on your infrastructure preferences and capabilities: + ### Option 1: AWS Deployment with Terraform -This is the recommended method, providing the best performance and uptime at the lowest cost. +This method leverages AWS services for optimal performance, scalability, and cost-effectiveness. -[TerraForm setup](./terraform/README.md) +**Advantages:** +- Most cost-effective solution at scale +- Guaranteed 100% uptime with AWS reliability +- Optimized for performance with managed services +- Automatic scaling based on demand +While this option may be more technically complex to set up initially, it provides the best long-term solution for dedicated providers. -### Option 2: Virtual Machine Deployment with Docker Compose -This option may cost more and depends on the uptime of the hardware you use. It is not recommended for long-term or high-performance node operators. +[Terraform setup guide](./terraform/README.md) -[Docker-compose setup](./docker-compose/README.md) +### Option 2: Docker Compose Deployment +This method allows you to run the provider on your own hardware using Docker containers. +**Advantages:** +- Easier to set up if you have spare hardware available +- More straightforward for users familiar with Docker +- Direct control over your infrastructure +- Simpler technical requirements -## Graceful Shutdown Policy -To avoid being slashed, it is critical to run the graceful shutdown in the event of downtime. Failing to do so may result in penalties. +This option is less resource-intensive once you've mined enough time lock puzzles and stored them for sale. -To run this go to ar://randao and navigate to your node and select the "SHUT DOWN" button and sign the transaction. -This will tell your provider to stop serving random. +[Docker Compose setup guide](./docker-compose/README.md) -After the maintinance is done and your provider is back up again click "START UP" button. -This will tell your provider to start serving random. +Both methods are fully supported and recommended as long as you can ensure 100% uptime or follow the graceful shutdown procedure during maintenance periods. --- +## Graceful Shutdown Policy +To avoid being slashed, it is critical to run the graceful shutdown in the event of planned downtime. Failing to do so may result in penalties. + +To run a graceful shutdown: +1. Go to ar://randao +2. Navigate to your node +3. Select the "SHUT DOWN" button and sign the transaction + +This will tell your provider to stop serving random values. + +After maintenance is complete and your provider is back online: +1. Click the "START UP" button +2. This will signal your provider to resume serving random values + +--- ## Staking -After successfully setting up your node, you will need to provide proof that the gateway is operational. +After successfully setting up your node, you will need to provide proof that the gateway is operational: 1. Show logs from the node setup to Ethan for verification. - 2. Upon confirmation, Ethan will provide your provider address with the necessary funds. - 3. Navigate to ar://randao to stake your funds and configure your provider information. By completing this process, you will fully activate your node and ensure it is ready for network participation. -Thank you for contributing to the network's success! \ No newline at end of file + +Thank you for contributing to the network's success! diff --git a/docker-compose/README.md b/docker-compose/README.md index 28bf61c..3600fd8 100644 --- a/docker-compose/README.md +++ b/docker-compose/README.md @@ -1,33 +1,81 @@ -# Steps to Deploy: -1. **Install Docker Compose:** - Follow the [official Docker Compose installation guide](https://docs.docker.com/compose/install/). +# Docker Compose Setup Guide +This guide walks you through deploying a randomness provider using Docker Compose on your own hardware. -2. **Deploy Node:** - Navigate to the Docker Compose directory and run: +## Prerequisites + +- A machine meeting the minimum hardware requirements (4 GB memory, 2 CPU cores) +- Reliable internet connection +- Ability to ensure 100% uptime or follow graceful shutdown procedures + +## Steps to Deploy + +1. **Install Docker and Docker Compose** + Follow the [official Docker Compose installation guide](https://docs.docker.com/compose/install/) for your operating system. + +2. **Configure Environment Variables** + Navigate to the Docker Compose directory and create your environment file: ```bash cp .env.example .env ``` -Then fill in all of the variables: -Required variables: -- `provider_id`: Your unique provider identifier -- `local_db_user`: Database username -- `local_db_password`: Database password -- `local_wallet_json`: Wallet JSON (either direct or via file) - -Optional variables (with defaults): -- `aws_region`: AWS region (default: us-east-1) -- `db_name`: Database name (default: orchestrator_db) -- `secrets_prefix`: Prefix for secrets (default: /orchestrator) - -3. **Deploy Node:** - Navigate to the Docker Compose directory and run: + + Then edit the `.env` file and fill in all required variables: + + **Required Variables:** + - `provider_id`: Your unique provider identifier + - `local_db_user`: Database username + - `local_db_password`: Database password + - `local_wallet_json`: Wallet JSON (either direct or via file path) + + **Optional Variables (with defaults):** + - `aws_region`: AWS region (default: us-east-1) + - `db_name`: Database name (default: orchestrator_db) + - `secrets_prefix`: Prefix for secrets (default: /orchestrator) + +3. **Deploy Your Provider** + From the Docker Compose directory, run: ```bash docker-compose up -d ``` -This setup will work but may not guarantee the same performance or reliability as the AWS-based deployment. +4. **Verify Deployment** + Check the status of your containers: + ```bash + docker-compose ps + ``` + + View logs to ensure everything is running correctly: + ```bash + docker-compose logs -f + ``` + +## How It Works + +This deployment creates three containerized services: + +1. **Provider Service**: Handles the main provider functionality and communicates with the blockchain +2. **Database**: Stores cryptographic time lock puzzles and provider state +3. **Puzzle Generator**: Creates time lock puzzles through the "mining" process + +## Advantages of Docker Compose + +- **Easier Setup**: More straightforward for those with existing hardware +- **Direct Control**: Full control over your infrastructure +- **Simplified Management**: Easy to manage with standard Docker commands +- **Lower Technical Barrier**: Simpler for those familiar with containerization + +## Maintenance + +Remember to follow the graceful shutdown procedure in the main documentation when performing maintenance on your Docker-based provider. Never kill the containers without proper shutdown or you risk being slashed. + +To update your provider when new versions are released: + +```bash +docker-compose pull +docker-compose down +docker-compose up -d +``` --- -[Main docs](../README.md) \ No newline at end of file +[Return to Main Documentation](../README.md) diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 77f3ad5..6b9b1a1 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -13,18 +13,28 @@ const AO_CONFIG = { }; let randomClientInstance: RandomClient | null = null; +let lastInitTime: number = 0; +const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds async function getRandomClient(): Promise { - - if (!randomClientInstance) { + const currentTime = Date.now(); + + if (!randomClientInstance || (currentTime - lastInitTime) > REINIT_INTERVAL) { randomClientInstance = ((await RandomClient.defaultBuilder()) .withAOConfig(AO_CONFIG)) .withWallet(JSON.parse(process.env.WALLET_JSON!)) .build(); + lastInitTime = currentTime; } + return randomClientInstance; } +// Optional: Auto-reinitialize on a timer +setInterval(() => { + randomClientInstance = null; +}, REINIT_INTERVAL); + const docker = new Docker(); // Constants for configuration diff --git a/terraform/README.md b/terraform/README.md index 45e7540..931ab97 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -1,107 +1,117 @@ -#### Steps to Deploy: -1. **Install Terraform:** +# AWS Terraform Setup Guide + +This guide walks you through deploying a randomness provider using Terraform and AWS cloud services for optimal performance and reliability. + +## Prerequisites + +- An AWS account with appropriate permissions +- Basic familiarity with AWS services and Terraform +- Terraform installed on your local machine + +## Steps to Deploy + +1. **Install Terraform** Follow the [official Terraform installation guide](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli). -2. **Log into AWS and set up IAM user and Policy** - Go To IAM and create a new policy called RandAO-Provider-Admin - Paste this in the JSON - ```{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "ecs:CreateCluster", - "ecs:DeleteCluster", - "ecs:CreateService", - "ecs:DeleteService", - "ecs:UpdateService", - "ecs:RegisterTaskDefinition", - "ecs:DeregisterTaskDefinition", - "ecs:ListTaskDefinitions", - "ecs:DescribeTaskDefinition", - "ecs:PutClusterCapacityProviders", - "ecs:DescribeClusters" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "iam:CreateRole", - "iam:DeleteRole", - "iam:GetRole", - "iam:PutRolePolicy", - "iam:DeleteRolePolicy", - "iam:AttachRolePolicy", - "iam:DetachRolePolicy", - "iam:PassRole" - ], - "Resource": "arn:aws:iam::*:role/orchestrator-*" - }, - { - "Effect": "Allow", - "Action": [ - "secretsmanager:CreateSecret", - "secretsmanager:DeleteSecret", - "secretsmanager:GetSecretValue", - "secretsmanager:PutSecretValue", - "secretsmanager:UpdateSecret", - "secretsmanager:TagResource" - ], - "Resource": "arn:aws:secretsmanager:*:*:secret:/orchestrator/*" - }, - { - "Effect": "Allow", - "Action": [ - "rds:CreateDBInstance", - "rds:DeleteDBInstance", - "rds:ModifyDBInstance", - "rds:DescribeDBInstances", - "rds:CreateDBSubnetGroup", - "rds:DeleteDBSubnetGroup", - "rds:ModifyDBSubnetGroup", - "rds:AddTagsToResource" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:DeleteLogGroup", - "logs:PutRetentionPolicy" - ], - "Resource": "arn:aws:logs:*:*:log-group:*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateSecurityGroup", - "ec2:DeleteSecurityGroup", - "ec2:AuthorizeSecurityGroupIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:CreateVpcEndpoint", - "ec2:DeleteVpcEndpoints", - "ec2:DescribeVpcEndpoints", - "ec2:DescribeSecurityGroups", - "ec2:DescribeNetworkInterfaces", - "ec2:CreateTags" - ], - "Resource": "*" - } - ] -} -``` - Save the JSON and name it - Click on Users and create a new user called TeraformDeployer - Attach this new policy directly - Save the User - Click on the user and go to the Security Credentials tab - Create an access key and choose CLI - Save the variables for the next step - -3. **Configure AWS Environment Variables:** +2. **Set Up IAM User and Policy** + 1. Go to AWS IAM and create a new policy called `RandAO-Provider-Admin` + 2. Select the JSON tab and paste this policy: + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ecs:CreateCluster", + "ecs:DeleteCluster", + "ecs:CreateService", + "ecs:DeleteService", + "ecs:UpdateService", + "ecs:RegisterTaskDefinition", + "ecs:DeregisterTaskDefinition", + "ecs:ListTaskDefinitions", + "ecs:DescribeTaskDefinition", + "ecs:PutClusterCapacityProviders", + "ecs:DescribeClusters" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "iam:CreateRole", + "iam:DeleteRole", + "iam:GetRole", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PassRole" + ], + "Resource": "arn:aws:iam::*:role/orchestrator-*" + }, + { + "Effect": "Allow", + "Action": [ + "secretsmanager:CreateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:GetSecretValue", + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecret", + "secretsmanager:TagResource" + ], + "Resource": "arn:aws:secretsmanager:*:*:secret:/orchestrator/*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:CreateDBInstance", + "rds:DeleteDBInstance", + "rds:ModifyDBInstance", + "rds:DescribeDBInstances", + "rds:CreateDBSubnetGroup", + "rds:DeleteDBSubnetGroup", + "rds:ModifyDBSubnetGroup", + "rds:AddTagsToResource" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:PutRetentionPolicy" + ], + "Resource": "arn:aws:logs:*:*:log-group:*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateSecurityGroup", + "ec2:DeleteSecurityGroup", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress", + "ec2:CreateVpcEndpoint", + "ec2:DeleteVpcEndpoints", + "ec2:DescribeVpcEndpoints", + "ec2:DescribeSecurityGroups", + "ec2:DescribeNetworkInterfaces", + "ec2:CreateTags" + ], + "Resource": "*" + } + ] + } + ``` + 3. Save the policy and name it + 4. Go to Users and create a new user called `TerraformDeployer` + 5. Attach the `RandAO-Provider-Admin` policy directly + 6. Create an access key for the user (choose CLI) + 7. Save the access key ID and secret access key for the next step + +3. **Configure AWS Environment Variables** Open a terminal and enter the following commands: ```bash export AWS_ACCESS_KEY_ID="your-access-key-id" @@ -109,30 +119,27 @@ export AWS_REGION="your-region" # e.g., us-east-1 ``` - -4. **Set up ENV variables:** +4. **Set Up Terraform Variables** Navigate to the Terraform directory of the project: ```bash cp terraform.tfvars.example terraform.tfvars ``` - Fill in all of the variables with your info. -## Variables Reference + Edit the `terraform.tfvars` file and fill in all required variables: -Required variables: -- `provider_id`: Your unique provider identifier -- `local_db_user`: Database username -- `local_db_password`: Database password -- `local_wallet_json`: Wallet JSON (either direct or via file) + **Required Variables:** + - `provider_id`: Your unique provider identifier + - `local_db_user`: Database username + - `local_db_password`: Database password + - `local_wallet_json`: Wallet JSON (either direct or via file path) -Optional variables (with defaults): -- `aws_region`: AWS region (default: us-east-1) -- `db_name`: Database name (default: orchestrator_db) -- `secrets_prefix`: Prefix for secrets (default: /orchestrator) + **Optional Variables (with defaults):** + - `aws_region`: AWS region (default: us-east-1) + - `db_name`: Database name (default: orchestrator_db) + - `secrets_prefix`: Prefix for secrets (default: /orchestrator) - The Database configuration is highly suggested and the secrets can be left alone as its just the name of the secrets + The database configuration is highly suggested, and the secrets can be left at default values as they're just the names of the secrets. - -5. **Initialize and Apply Terraform Configuration:** +5. **Initialize and Apply Terraform Configuration** Navigate to the Terraform directory of the project and run: ```bash terraform init @@ -140,19 +147,26 @@ Optional variables (with defaults): ``` Type `yes` when prompted to confirm. -This setup ensures your node is deployed with the highest uptime and optimal performance. +6. **Verify Deployment** + Open the AWS console and navigate to the ECS console to view your running services. -Please open up the AWS console and show the logs of this to Ethan top receive the Tokens to stake +## How It Works +This deployment creates the following AWS resources: -# Terraform Configuration for Randomness Provider +1. **ECS Cluster**: Runs the orchestrator service and puzzle generator jobs +2. **RDS**: PostgreSQL database for storing time lock puzzles and provider state +3. **Secrets Manager**: Securely stores database credentials and wallet information +4. **IAM**: Roles and policies for ECS tasks and secrets access +5. **CloudWatch**: Log groups for monitoring your provider -This Terraform configuration sets up: -- One orchestrator service on ECS Fargate -- One PostgreSQL RDS instance -- VDF Fargate spot job configuration -- AWS Secrets Manager for sensitive data +## Advantages of AWS Deployment +- **Cost Efficiency at Scale**: Most cost-effective for dedicated providers +- **Maximum Reliability**: AWS services offer SLAs for high availability +- **Automatic Scaling**: Resources scale based on demand +- **Managed Services**: AWS handles infrastructure maintenance +- **High Performance**: Optimized for speed and reliability ## Security Notes @@ -161,14 +175,10 @@ This Terraform configuration sets up: - In production, sensitive values are stored in AWS Secrets Manager - Local development variables are only used for initial setup and testing -## Infrastructure Components - -- **ECS Cluster**: Runs the orchestrator service and VDF jobs -- **RDS**: PostgreSQL database for the orchestrator -- **Secrets Manager**: Securely stores database credentials and wallet -- **IAM**: Roles and policies for ECS tasks and secrets access -- **CloudWatch**: Log groups for monitoring +## Staking Process +After deployment, please open the AWS console and share the logs with Ethan to receive the tokens needed for staking. Follow the main documentation for the staking process. +--- -[Main docs](../README.md) \ No newline at end of file +[Return to Main Documentation](../README.md) From f5d65a4e5db63669f7b2ee6c62226eb57b615f5c Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 1 Apr 2025 20:26:24 -0400 Subject: [PATCH 42/80] added docs --- docker-compose/README.md | 47 ++++++++++++++++++++++ docker-compose/mass-dev-docker-compose.yml | 2 +- orchestrator/src/app.ts | 5 ++- requester/docs/development.md | 2 +- requester/src/app.ts | 2 +- 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/docker-compose/README.md b/docker-compose/README.md index 3600fd8..fa60c4a 100644 --- a/docker-compose/README.md +++ b/docker-compose/README.md @@ -64,6 +64,53 @@ This deployment creates three containerized services: - **Simplified Management**: Easy to manage with standard Docker commands - **Lower Technical Barrier**: Simpler for those familiar with containerization +## Troubleshooting + +If you encounter issues with your provider, here are some common problems and solutions: + +### "Provider Not Found" Error +- Ensure your provider is properly staked at https://providers_randao.ar.io +- Wait for blockchain confirmation as it may take some time for your stake to be recognized +- Check your wallet configuration in the `.env` file + +### Network Connectivity Issues +- If your provider can't connect to the network, it may be due to network congestion +- Wait for network conditions to improve before attempting to restart +- Check your internet connection and firewall settings + +### Slow or Unresponsive Provider +- Check system resources to ensure your host has sufficient CPU and memory +- Monitor the logs for any error messages or warnings: + ```bash + docker-compose logs -f + ``` +- If the puzzle generator is struggling, consider scaling up your hardware + +### General Issues +- Try restarting the containers: + ```bash + docker-compose restart + ``` +- For more persistent issues, you can try a full reset: + ```bash + docker-compose down + docker-compose up -d + ``` +- Ensure your container has the latest version: + ```bash + docker-compose pull + docker-compose down + docker-compose up -d + ``` + +### Database Issues +- If the database container fails to start, check logs for specific errors: + ```bash + docker-compose logs db + ``` +- Ensure the database password in your `.env` file doesn't contain special characters that need escaping +- Verify database volume permissions if running on Linux + ## Maintenance Remember to follow the graceful shutdown procedure in the main documentation when performing maintenance on your Docker-based provider. Never kill the containers without proper shutdown or you risk being slashed. diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml index f2c0528..fc88394 100644 --- a/docker-compose/mass-dev-docker-compose.yml +++ b/docker-compose/mass-dev-docker-compose.yml @@ -599,7 +599,7 @@ services: - dbeaver-data:/opt/cloudbeaver/workspace requester: - image: randao/requester:v0.4.2 + image: randao/requester:v0.4.5 environment: REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 6b9b1a1..91875a3 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -38,7 +38,8 @@ setInterval(() => { const docker = new Docker(); // Constants for configuration -const POLLING_INTERVAL_MS = 10000; +const POLLING_INTERVAL_MS = 30000; //30 seconds +const DATABASE_CHECK_TIME = 60000; //60 seconds const MINIMUM_ENTRIES = 1000; const DRYRUNTIMEOUT = 30000; // 30 seconds const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; @@ -881,7 +882,7 @@ async function run(): Promise { console.error("Error in checkAndFetchIfNeeded:", error); }); - }, 10000); + }, DATABASE_CHECK_TIME); setInterval(async () => { await monitorDockerContainers(); diff --git a/requester/docs/development.md b/requester/docs/development.md index a275bc2..412c1fb 100644 --- a/requester/docs/development.md +++ b/requester/docs/development.md @@ -2,4 +2,4 @@ To build: Save all files Run: -docker build -t randao/requester:latest -t randao/requester:v0.4.0 . \ No newline at end of file +docker build -t randao/requester:latest -t randao/requester:v0.4.5 . \ No newline at end of file diff --git a/requester/src/app.ts b/requester/src/app.ts index e20b5ff..c79b655 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -9,7 +9,7 @@ import { RandomClientConfigBuilder, } from "ao-process-clients"; -const RETRY_DELAY_MS = 10000; // 10 seconds +const RETRY_DELAY_MS = 30000; // 30 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; From 26e206312d350d4a5c7b03ff34423b2dee506a2e Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 4 Apr 2025 10:10:29 -0400 Subject: [PATCH 43/80] cleaned up --- docker-compose/dev-docker-compose.yml | 154 ----- docker-compose/docker-compose.yml | 1 - docker-compose/mass-dev-docker-compose.yml | 643 --------------------- requester/src/app.ts | 2 +- 4 files changed, 1 insertion(+), 799 deletions(-) delete mode 100644 docker-compose/dev-docker-compose.yml delete mode 100644 docker-compose/mass-dev-docker-compose.yml diff --git a/docker-compose/dev-docker-compose.yml b/docker-compose/dev-docker-compose.yml deleted file mode 100644 index 5b5d1ad..0000000 --- a/docker-compose/dev-docker-compose.yml +++ /dev/null @@ -1,154 +0,0 @@ -# version: '3.8' - -services: - # Instance 1 - postgres1: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_1:-myuser1} - POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword1} - POSTGRES_DB: ${DB_NAME_1:-mydatabase1} - ports: - - "5432:5432" - networks: - - backend - volumes: - - pgdata1:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser1} -d ${DB_NAME_1:-mydatabase1}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator1: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres1: - condition: service_healthy - environment: - DB_HOST: postgres1 - DB_PORT: 5432 - DB_USER: ${DB_USER_1:-myuser1} - DB_PASSWORD: ${DB_PASSWORD_1:-mypassword1} - DB_NAME: ${DB_NAME_1:-mydatabase1} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_1} - PROVIDER_ID: ${PROVIDER_ID_1} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 2 - postgres2: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_2:-myuser2} - POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword2} - POSTGRES_DB: ${DB_NAME_2:-mydatabase2} - ports: - - "5433:5432" - networks: - - backend - volumes: - - pgdata2:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser2} -d ${DB_NAME_2:-mydatabase2}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator2: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres2: - condition: service_healthy - environment: - DB_HOST: postgres2 - DB_PORT: 5432 - DB_USER: ${DB_USER_2:-myuser2} - DB_PASSWORD: ${DB_PASSWORD_2:-mypassword2} - DB_NAME: ${DB_NAME_2:-mydatabase2} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_2} - PROVIDER_ID: ${PROVIDER_ID_2} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 3 - postgres3: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_3:-myuser3} - POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword3} - POSTGRES_DB: ${DB_NAME_3:-mydatabase3} - ports: - - "5434:5432" - networks: - - backend - volumes: - - pgdata3:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser3} -d ${DB_NAME_3:-mydatabase3}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator3: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres3: - condition: service_healthy - environment: - DB_HOST: postgres3 - DB_PORT: 5432 - DB_USER: ${DB_USER_3:-myuser3} - DB_PASSWORD: ${DB_PASSWORD_3:-mypassword3} - DB_NAME: ${DB_NAME_3:-mydatabase3} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_3} - PROVIDER_ID: ${PROVIDER_ID_3} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Single DBeaver Instance - dbeaver: - image: dbeaver/cloudbeaver:23.2.0 - environment: - CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin" - CB_SERVER_ADMIN_PASSWORD: "admin123" - networks: - - backend - ports: - - "8080:8978" - volumes: - - dbeaver-data:/opt/cloudbeaver/workspace - - requester: - image: randao/requester:v0.2.8 - environment: - REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - - -networks: - backend: - name: backend - driver: bridge - -volumes: - pgdata1: - driver: local - pgdata2: - driver: local - pgdata3: - driver: local - dbeaver-data: - driver: local diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 120c8a8..4fff7ae 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -31,7 +31,6 @@ services: ENVIRONMENT: local PATH_TO_WALLET: /app/wallet.json # Path inside the container WALLET_JSON: ${WALLET_JSON} - PROVIDER_ID: ${PROVIDER_ID} DOCKER_NETWORK: backend # Passing the network name networks: - backend diff --git a/docker-compose/mass-dev-docker-compose.yml b/docker-compose/mass-dev-docker-compose.yml deleted file mode 100644 index fc88394..0000000 --- a/docker-compose/mass-dev-docker-compose.yml +++ /dev/null @@ -1,643 +0,0 @@ -services: - # Instance 1 - postgres1: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_1:-myuser1} - POSTGRES_PASSWORD: ${DB_PASSWORD_1:-mypassword1} - POSTGRES_DB: ${DB_NAME_1:-mydatabase1} - ports: - - "5432:5432" - networks: - - backend - volumes: - - pgdata1:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_1:-myuser1} -d ${DB_NAME_1:-mydatabase1}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator1: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres1: - condition: service_healthy - environment: - DB_HOST: postgres1 - DB_PORT: 5432 - DB_USER: ${DB_USER_1:-myuser1} - DB_PASSWORD: ${DB_PASSWORD_1:-mypassword1} - DB_NAME: ${DB_NAME_1:-mydatabase1} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_1} - PROVIDER_ID: ${PROVIDER_ID_1} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 2 - postgres2: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_2:-myuser2} - POSTGRES_PASSWORD: ${DB_PASSWORD_2:-mypassword2} - POSTGRES_DB: ${DB_NAME_2:-mydatabase2} - ports: - - "5433:5432" - networks: - - backend - volumes: - - pgdata2:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_2:-myuser2} -d ${DB_NAME_2:-mydatabase2}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator2: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres2: - condition: service_healthy - environment: - DB_HOST: postgres2 - DB_PORT: 5432 - DB_USER: ${DB_USER_2:-myuser2} - DB_PASSWORD: ${DB_PASSWORD_2:-mypassword2} - DB_NAME: ${DB_NAME_2:-mydatabase2} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_2} - PROVIDER_ID: ${PROVIDER_ID_2} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 3 - postgres3: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_3:-myuser3} - POSTGRES_PASSWORD: ${DB_PASSWORD_3:-mypassword3} - POSTGRES_DB: ${DB_NAME_3:-mydatabase3} - ports: - - "5434:5432" - networks: - - backend - volumes: - - pgdata3:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_3:-myuser3} -d ${DB_NAME_3:-mydatabase3}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator3: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres3: - condition: service_healthy - environment: - DB_HOST: postgres3 - DB_PORT: 5432 - DB_USER: ${DB_USER_3:-myuser3} - DB_PASSWORD: ${DB_PASSWORD_3:-mypassword3} - DB_NAME: ${DB_NAME_3:-mydatabase3} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_3} - PROVIDER_ID: ${PROVIDER_ID_3} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 4 - postgres4: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_4:-myuser4} - POSTGRES_PASSWORD: ${DB_PASSWORD_4:-mypassword4} - POSTGRES_DB: ${DB_NAME_4:-mydatabase4} - ports: - - "5435:5432" - networks: - - backend - volumes: - - pgdata4:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_4:-myuser4} -d ${DB_NAME_4:-mydatabase4}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator4: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres4: - condition: service_healthy - environment: - DB_HOST: postgres4 - DB_PORT: 5432 - DB_USER: ${DB_USER_4:-myuser4} - DB_PASSWORD: ${DB_PASSWORD_4:-mypassword4} - DB_NAME: ${DB_NAME_4:-mydatabase4} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_4} - PROVIDER_ID: ${PROVIDER_ID_4} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 5 - postgres5: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_5:-myuser5} - POSTGRES_PASSWORD: ${DB_PASSWORD_5:-mypassword5} - POSTGRES_DB: ${DB_NAME_5:-mydatabase5} - ports: - - "5436:5432" - networks: - - backend - volumes: - - pgdata5:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_5:-myuser5} -d ${DB_NAME_5:-mydatabase5}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator5: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres5: - condition: service_healthy - environment: - DB_HOST: postgres5 - DB_PORT: 5432 - DB_USER: ${DB_USER_5:-myuser5} - DB_PASSWORD: ${DB_PASSWORD_5:-mypassword5} - DB_NAME: ${DB_NAME_5:-mydatabase5} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_5} - PROVIDER_ID: ${PROVIDER_ID_5} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 6 - postgres6: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_6:-myuser6} - POSTGRES_PASSWORD: ${DB_PASSWORD_6:-mypassword6} - POSTGRES_DB: ${DB_NAME_6:-mydatabase6} - ports: - - "5437:5432" - networks: - - backend - volumes: - - pgdata6:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_6:-myuser6} -d ${DB_NAME_6:-mydatabase6}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator6: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres6: - condition: service_healthy - environment: - DB_HOST: postgres6 - DB_PORT: 5432 - DB_USER: ${DB_USER_6:-myuser6} - DB_PASSWORD: ${DB_PASSWORD_6:-mypassword6} - DB_NAME: ${DB_NAME_6:-mydatabase6} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_6} - PROVIDER_ID: ${PROVIDER_ID_6} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 7 - postgres7: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_7:-myuser7} - POSTGRES_PASSWORD: ${DB_PASSWORD_7:-mypassword7} - POSTGRES_DB: ${DB_NAME_7:-mydatabase7} - ports: - - "5438:5432" - networks: - - backend - volumes: - - pgdata7:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_7:-myuser7} -d ${DB_NAME_7:-mydatabase7}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator7: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres7: - condition: service_healthy - environment: - DB_HOST: postgres7 - DB_PORT: 5432 - DB_USER: ${DB_USER_7:-myuser7} - DB_PASSWORD: ${DB_PASSWORD_7:-mypassword7} - DB_NAME: ${DB_NAME_7:-mydatabase7} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_7} - PROVIDER_ID: ${PROVIDER_ID_7} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 8 - postgres8: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_8:-myuser8} - POSTGRES_PASSWORD: ${DB_PASSWORD_8:-mypassword8} - POSTGRES_DB: ${DB_NAME_8:-mydatabase8} - ports: - - "5439:5432" - networks: - - backend - volumes: - - pgdata8:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_8:-myuser8} -d ${DB_NAME_8:-mydatabase8}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator8: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres8: - condition: service_healthy - environment: - DB_HOST: postgres8 - DB_PORT: 5432 - DB_USER: ${DB_USER_8:-myuser8} - DB_PASSWORD: ${DB_PASSWORD_8:-mypassword8} - DB_NAME: ${DB_NAME_8:-mydatabase8} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_8} - PROVIDER_ID: ${PROVIDER_ID_8} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 9 - postgres9: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_9:-myuser9} - POSTGRES_PASSWORD: ${DB_PASSWORD_9:-mypassword9} - POSTGRES_DB: ${DB_NAME_9:-mydatabase9} - ports: - - "5440:5432" - networks: - - backend - volumes: - - pgdata9:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_9:-myuser9} -d ${DB_NAME_9:-mydatabase9}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator9: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres9: - condition: service_healthy - environment: - DB_HOST: postgres9 - DB_PORT: 5432 - DB_USER: ${DB_USER_9:-myuser9} - DB_PASSWORD: ${DB_PASSWORD_9:-mypassword9} - DB_NAME: ${DB_NAME_9:-mydatabase9} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_9} - PROVIDER_ID: ${PROVIDER_ID_9} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 10 - postgres10: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_10:-myuser10} - POSTGRES_PASSWORD: ${DB_PASSWORD_10:-mypassword10} - POSTGRES_DB: ${DB_NAME_10:-mydatabase10} - ports: - - "5441:5432" - networks: - - backend - volumes: - - pgdata10:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_10:-myuser10} -d ${DB_NAME_10:-mydatabase10}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator10: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres10: - condition: service_healthy - environment: - DB_HOST: postgres10 - DB_PORT: 5432 - DB_USER: ${DB_USER_10:-myuser10} - DB_PASSWORD: ${DB_PASSWORD_10:-mypassword10} - DB_NAME: ${DB_NAME_10:-mydatabase10} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_10} - PROVIDER_ID: ${PROVIDER_ID_10} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 11 - postgres11: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_11:-myuser11} - POSTGRES_PASSWORD: ${DB_PASSWORD_11:-mypassword11} - POSTGRES_DB: ${DB_NAME_11:-mydatabase11} - ports: - - "5442:5432" - networks: - - backend - volumes: - - pgdata11:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_11:-myuser11} -d ${DB_NAME_11:-mydatabase11}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator11: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres11: - condition: service_healthy - environment: - DB_HOST: postgres11 - DB_PORT: 5432 - DB_USER: ${DB_USER_11:-myuser11} - DB_PASSWORD: ${DB_PASSWORD_11:-mypassword11} - DB_NAME: ${DB_NAME_11:-mydatabase11} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_11} - PROVIDER_ID: ${PROVIDER_ID_11} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 12 - postgres12: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_12:-myuser12} - POSTGRES_PASSWORD: ${DB_PASSWORD_12:-mypassword12} - POSTGRES_DB: ${DB_NAME_12:-mydatabase12} - ports: - - "5443:5432" - networks: - - backend - volumes: - - pgdata12:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_12:-myuser12} -d ${DB_NAME_12:-mydatabase12}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator12: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres12: - condition: service_healthy - environment: - DB_HOST: postgres12 - DB_PORT: 5432 - DB_USER: ${DB_USER_12:-myuser12} - DB_PASSWORD: ${DB_PASSWORD_12:-mypassword12} - DB_NAME: ${DB_NAME_12:-mydatabase12} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_12} - PROVIDER_ID: ${PROVIDER_ID_12} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 13 - postgres13: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_13:-myuser13} - POSTGRES_PASSWORD: ${DB_PASSWORD_13:-mypassword13} - POSTGRES_DB: ${DB_NAME_13:-mydatabase13} - ports: - - "5444:5432" - networks: - - backend - volumes: - - pgdata13:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_13:-myuser13} -d ${DB_NAME_13:-mydatabase13}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator13: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres13: - condition: service_healthy - environment: - DB_HOST: postgres13 - DB_PORT: 5432 - DB_USER: ${DB_USER_13:-myuser13} - DB_PASSWORD: ${DB_PASSWORD_13:-mypassword13} - DB_NAME: ${DB_NAME_13:-mydatabase13} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_13} - PROVIDER_ID: ${PROVIDER_ID_13} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 14 - postgres14: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_14:-myuser14} - POSTGRES_PASSWORD: ${DB_PASSWORD_14:-mypassword14} - POSTGRES_DB: ${DB_NAME_14:-mydatabase14} - ports: - - "5445:5432" - networks: - - backend - volumes: - - pgdata14:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_14:-myuser14} -d ${DB_NAME_14:-mydatabase14}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator14: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres14: - condition: service_healthy - environment: - DB_HOST: postgres14 - DB_PORT: 5432 - DB_USER: ${DB_USER_14:-myuser14} - DB_PASSWORD: ${DB_PASSWORD_14:-mypassword14} - DB_NAME: ${DB_NAME_14:-mydatabase14} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_14} - PROVIDER_ID: ${PROVIDER_ID_14} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Instance 15 - postgres15: - image: postgres:13 - environment: - POSTGRES_USER: ${DB_USER_15:-myuser15} - POSTGRES_PASSWORD: ${DB_PASSWORD_15:-mypassword15} - POSTGRES_DB: ${DB_NAME_15:-mydatabase15} - ports: - - "5446:5432" - networks: - - backend - volumes: - - pgdata15:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER_15:-myuser15} -d ${DB_NAME_15:-mydatabase15}"] - interval: 10s - timeout: 5s - retries: 5 - - orchestrator15: - image: randao/orchestrator:v0.4.55 - depends_on: - postgres15: - condition: service_healthy - environment: - DB_HOST: postgres15 - DB_PORT: 5432 - DB_USER: ${DB_USER_15:-myuser15} - DB_PASSWORD: ${DB_PASSWORD_15:-mypassword15} - DB_NAME: ${DB_NAME_15:-mydatabase15} - ENVIRONMENT: local - WALLET_JSON: ${WALLET_JSON_15} - PROVIDER_ID: ${PROVIDER_ID_15} - DOCKER_NETWORK: backend - networks: - - backend - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # Single DBeaver Instance - dbeaver: - image: dbeaver/cloudbeaver:23.2.0 - environment: - CB_SERVER_SERVER_PORT: 8080 - CB_SERVER_ADMIN_NAME: "admin" - CB_SERVER_ADMIN_PASSWORD: "admin123" - networks: - - backend - ports: - - "8080:8978" - volumes: - - dbeaver-data:/opt/cloudbeaver/workspace - - requester: - image: randao/requester:v0.4.5 - environment: - REQUEST_WALLET_JSON: ${REQUEST_WALLET_JSON} - -networks: - backend: - name: backend - driver: bridge - -volumes: - pgdata1: - driver: local - pgdata2: - driver: local - pgdata3: - driver: local - pgdata4: - driver: local - pgdata5: - driver: local - pgdata6: - driver: local - pgdata7: - driver: local - pgdata8: - driver: local - pgdata9: - driver: local - pgdata10: - driver: local - pgdata11: - driver: local - pgdata12: - driver: local - pgdata13: - driver: local - pgdata14: - driver: local - pgdata15: - driver: local - dbeaver-data: - driver: local diff --git a/requester/src/app.ts b/requester/src/app.ts index c79b655..5837991 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -30,7 +30,7 @@ async function getRandomClient(): Promise { if (!randomClientInstance) { randomClientInstance = ((await RandomClient.defaultBuilder()) .withAOConfig(AO_CONFIG)) - .withProcessId("2ExUldxQ5NA_hnElSWYq0_lCBgeQQPxPhFbWDFihDEY") + // .withProcessId("2ExUldxQ5NA_hnElSWYq0_lCBgeQQPxPhFbWDFihDEY") .withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)) .build(); } From 0057ad386efa00f8acc8713e19e5475857b9adad Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 4 Apr 2025 10:11:15 -0400 Subject: [PATCH 44/80] cleaned up2 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8e1d01f..2b9af51 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ venv/ env *wallet.json *wallet*.json +mass-docker-compose.yml # Ignore distribution/build directories dist/ build/ From 8c6c562a5d134ac3d1cf6b56462b4dc32d253688 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 5 Apr 2025 14:13:44 -0400 Subject: [PATCH 45/80] Cleaned up docs --- README.md | 254 ++++-- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 774 +----------------- orchestrator/src/containerManagment.ts | 322 ++++++++ orchestrator/src/db_config.ts | 7 - orchestrator/src/db_tools.ts | 80 ++ orchestrator/src/ecs_config.ts | 72 -- orchestrator/src/helperFunctions.ts | 396 +++++++++ .../src/{ => oldjunk}/clear_outputs.tzs | 0 orchestrator/src/{ => oldjunk}/stake.tzs | 0 orchestrator/src/reset_db.ts | 4 +- orchestrator/tsconfig.json | 2 +- requester/src/app.ts | 9 +- terraform/.debug_task.tf | 36 - terraform/README.md | 184 ----- terraform/ecs.tf | 315 ------- terraform/iam.json | 87 -- terraform/iam_roles.tf | 83 -- terraform/locals.tf | 23 - terraform/logging.tf | 6 - terraform/outputs.tf | 18 - terraform/package.json | 8 - terraform/providers.tf | 16 - terraform/rds.tf | 66 -- terraform/secrets.tf | 62 -- terraform/security.tf | 33 - terraform/terraform.tfvars.example | 31 - terraform/variables.tf | 61 -- terraform/vpc_endpoints.tf | 116 --- 29 files changed, 1011 insertions(+), 2056 deletions(-) create mode 100644 orchestrator/src/containerManagment.ts delete mode 100644 orchestrator/src/db_config.ts create mode 100644 orchestrator/src/db_tools.ts delete mode 100644 orchestrator/src/ecs_config.ts create mode 100644 orchestrator/src/helperFunctions.ts rename orchestrator/src/{ => oldjunk}/clear_outputs.tzs (100%) rename orchestrator/src/{ => oldjunk}/stake.tzs (100%) delete mode 100644 terraform/.debug_task.tf delete mode 100644 terraform/README.md delete mode 100644 terraform/ecs.tf delete mode 100644 terraform/iam.json delete mode 100644 terraform/iam_roles.tf delete mode 100644 terraform/locals.tf delete mode 100644 terraform/logging.tf delete mode 100644 terraform/outputs.tf delete mode 100644 terraform/package.json delete mode 100644 terraform/providers.tf delete mode 100644 terraform/rds.tf delete mode 100644 terraform/secrets.tf delete mode 100644 terraform/security.tf delete mode 100644 terraform/terraform.tfvars.example delete mode 100644 terraform/variables.tf delete mode 100644 terraform/vpc_endpoints.tf diff --git a/README.md b/README.md index 209ef8c..1f3cd0a 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,235 @@ # Node Provider Setup Guide -This guide will walk you through getting your randomness provider set up and connected to the network so you can start contributing to the protocol and participating in decentralized randomness! +This guide will help you set up your randomness provider node and start earning rewards. The guide is split into a simple quickstart section followed by more detailed technical information. ## Table of Contents -1. [Introduction](#introduction) -2. [Hardware Requirements](#hardware-requirements) -3. [Randomness Generation](#randomness-generation) -4. [Deployment Options](#deployment-options) - - [Option 1: AWS Deployment with Terraform](#option-1-aws-deployment-with-terraform) - - [Option 2: Docker Compose Deployment](#option-2-docker-compose-deployment) -5. [Graceful Shutdown Policy](#graceful-shutdown-policy) -6. [Staking](#staking) +1. [Quickstart Guide](#quickstart-guide) +2. [How It Works](#how-it-works) +3. [Hardware Requirements](#hardware-requirements) +4. [Detailed Setup Instructions](#detailed-setup-instructions) +5. [Maintenance](#maintenance) +6. [Graceful Shutdown](#graceful-shutdown) +7. [Troubleshooting](#troubleshooting) +8. [Frequently Asked Questions](#frequently-asked-questions) --- -## Introduction -As a node provider, you are responsible for ensuring 100% uptime. In the event of necessary downtime, it is mandatory to run the graceful shutdown process to prevent being slashed. +## Quickstart Guide -Your provider performs 3 main functions: -1. It updates the amount of available random it has stored on chain. Each random value requires significant computation to create, ensuring compliance with our commit-reveal time delay scheme. -2. It detects when someone has requested random from you and provides the input number to your time delay function. This is not the final random number. -3. It detects when all parties have submitted their input numbers and then provides the output number along with the proof of history checkpoints to the chain for verification. This output is the random number that will be used on chain. - -These steps are executed as quickly as possible to get the complete random value on chain promptly. Faster providers will be incentivized for their speed, while slower ones will be penalized. If a provider is too slow for step 2, it will be slashed a small amount. If a provider is too slow for step 3, they will be considered malicious and slashed heavily. +Setting up your randomness provider is easy! Just follow these simple steps: -If you need to take your provider offline, you must run the graceful shutdown process, which will execute step 1 once with a value of -1, indicating you are no longer offering random. After that, your node will finish all pending requests in steps 2 and 3 before shutting down. +### Step 1: Install Docker +Install Docker and Docker Compose by following the [official Docker Compose installation guide](https://docs.docker.com/compose/install/) for your operating system. ---- +### Step 2: Set Up Your Environment +1. Navigate to the Docker Compose directory +2. Copy the example environment file: + ``` + cp .env.example .env + ``` +3. Edit the `.env` file and add your wallet information -## Hardware Requirements -To run a node, the following hardware specifications are required: - -- **Minimum Hardware Requirements:** - - 4 GB memory - - 2 CPU cores -- **Note:** These requirements will increase over time to meet network demands. - ---- +### Step 3: Start Your Provider +Run this command to start your provider: +``` +docker-compose up -d +``` -## Randomness Generation +### Step 4: Stake Your Node +1. Navigate to ar://randao +2. Connect your wallet +3. Follow the staking instructions to activate your provider -Our system now uses cryptographic time lock puzzles instead of Verifiable Delay Functions (VDF) for randomness generation. The provider "mines" for these puzzles, which creates a provable time delay between commitment and revelation of random values. This approach enhances security while maintaining verifiability of the randomness generated. +That's it! Your node is now running and will start generating randomness for the network. -The time lock puzzles require significant initial computation but become less resource-intensive once you've mined and stored a sufficient number of them for sale. This makes the provider more efficient over time as your puzzle inventory grows. +**Need help?** Check the [Troubleshooting](#troubleshooting) section or [Frequently Asked Questions](#frequently-asked-questions) below. --- -## What the Hardware Runs -The hardware you set up runs 3 key services: -1. A provider service -2. A database for the provider -3. Temporary jobs to generate random values and store them in the database +## How It Works -Both of our deployment options are designed to run these services efficiently. +Your provider performs 3 main functions: +1. It creates and stores random values that others can request +2. It responds when someone requests a random value +3. It submits final verified random values to the blockchain + +The better your provider performs these functions, the more rewards you'll receive. Providers with faster response times earn more! --- -## Deployment Options +## Hardware Requirements -We now fully support and recommend two deployment methods, depending on your infrastructure preferences and capabilities: +To run a node, you'll need: +- At least 4 GB memory +- At least 2 CPU cores +- Reliable internet connection -### Option 1: AWS Deployment with Terraform -This method leverages AWS services for optimal performance, scalability, and cost-effectiveness. +**Note:** These requirements may increase over time as the network grows. -**Advantages:** -- Most cost-effective solution at scale -- Guaranteed 100% uptime with AWS reliability -- Optimized for performance with managed services -- Automatic scaling based on demand +--- -While this option may be more technically complex to set up initially, it provides the best long-term solution for dedicated providers. +## Detailed Setup Instructions + +### Prerequisites +- A machine meeting the minimum hardware requirements +- Docker and Docker Compose installed +- Reliable internet connection +- Ability to ensure 100% uptime or follow the [Graceful Shutdown](#graceful-shutdown) procedures + +### Setup Steps + +1. **Configure Environment Variables** + Navigate to the Docker Compose directory and create your environment file: + ```bash + cp .env.example .env + ``` + + Then edit the `.env` file and fill in all required variables: + + **Required Variables:** + - `provider_id`: Your unique provider identifier + - `local_db_user`: Database username + - `local_db_password`: Database password + - `local_wallet_json`: Your wallet information (either directly pasted or as a file path) + + **Optional Variables (with defaults):** + - `db_name`: Database name (default: orchestrator_db) + - `secrets_prefix`: Prefix for secrets (default: /orchestrator) + +2. **Deploy Your Provider** + From the Docker Compose directory, run: + ```bash + docker-compose up -d + ``` + +3. **Verify Deployment** + Check the status of your containers: + ```bash + docker-compose ps + ``` + + View logs to ensure everything is running correctly: + ```bash + docker-compose logs -f + ``` + +4. **Staking** + After successfully setting up your node: + 1. Show logs from the node setup to Ethan for verification + 2. Upon confirmation, Ethan will provide your provider address with the necessary funds + 3. Navigate to ar://randao to stake your funds and configure your provider information -[Terraform setup guide](./terraform/README.md) +By completing this process, you will fully activate your node and ensure it is ready for network participation. -### Option 2: Docker Compose Deployment -This method allows you to run the provider on your own hardware using Docker containers. +--- -**Advantages:** -- Easier to set up if you have spare hardware available -- More straightforward for users familiar with Docker -- Direct control over your infrastructure -- Simpler technical requirements +## Maintenance -This option is less resource-intensive once you've mined enough time lock puzzles and stored them for sale. +To update your provider when new versions are released: -[Docker Compose setup guide](./docker-compose/README.md) +```bash +docker-compose pull +docker-compose down +docker-compose up -d +``` -Both methods are fully supported and recommended as long as you can ensure 100% uptime or follow the graceful shutdown procedure during maintenance periods. +Remember to follow the graceful shutdown procedure when performing maintenance to avoid penalties. --- -## Graceful Shutdown Policy -To avoid being slashed, it is critical to run the graceful shutdown in the event of planned downtime. Failing to do so may result in penalties. +## Graceful Shutdown + +If you need to perform maintenance or temporarily shut down your provider, it's critical to follow these steps to avoid being penalized: -To run a graceful shutdown: 1. Go to ar://randao 2. Navigate to your node 3. Select the "SHUT DOWN" button and sign the transaction - -This will tell your provider to stop serving random values. +4. Wait for your provider to complete all pending requests (check logs) +5. Once all pending requests are complete, you can safely shut down your provider After maintenance is complete and your provider is back online: 1. Click the "START UP" button 2. This will signal your provider to resume serving random values +**Warning:** Failing to follow the graceful shutdown procedure may result in penalties to your stake! + --- -## Staking +## Troubleshooting + +If you encounter issues with your provider, here are some common problems and solutions: + +### "Provider Not Found" Error +- Ensure your provider is properly staked at ar://randao +- Wait for blockchain confirmation (it may take some time for your stake to be recognized) +- Check your wallet configuration in the `.env` file + +### Network Connectivity Issues +- If your provider can't connect to the network, check your internet connection +- Check your firewall settings to ensure the required ports are open +- Wait for network conditions to improve before attempting to restart + +### Slow or Unresponsive Provider +- Check system resources to ensure your host has sufficient CPU and memory +- Monitor the logs for any error messages or warnings: + ```bash + docker-compose logs -f + ``` +- If the puzzle generator is struggling, consider scaling up your hardware + +### General Issues +- Try restarting the containers: + ```bash + docker-compose restart + ``` +- For more persistent issues, you can try a full reset: + ```bash + docker-compose down + docker-compose up -d + ``` +- Ensure your container has the latest version: + ```bash + docker-compose pull + docker-compose down + docker-compose up -d + ``` + +### Database Issues +- If the database container fails to start, check logs for specific errors: + ```bash + docker-compose logs db + ``` +- Ensure the database password in your `.env` file doesn't contain special characters that need escaping +- Verify database volume permissions if running on Linux -After successfully setting up your node, you will need to provide proof that the gateway is operational: +--- -1. Show logs from the node setup to Ethan for verification. -2. Upon confirmation, Ethan will provide your provider address with the necessary funds. -3. Navigate to ar://randao to stake your funds and configure your provider information. +## Frequently Asked Questions -By completing this process, you will fully activate your node and ensure it is ready for network participation. +### What is a randomness provider? +A randomness provider generates verifiable random numbers that are used in various decentralized applications on the blockchain. These random numbers are crucial for fair and transparent operation of many applications. + +### How do I earn rewards? +You earn rewards by providing random values to users who request them. The rewards depend on your provider's performance, reliability, and response time. + +### What happens if my provider goes offline? +If your provider goes offline without following the graceful shutdown procedure, you may be penalized (slashed). Always follow the [Graceful Shutdown](#graceful-shutdown) procedure before taking your provider offline. + +### How much can I earn as a provider? +Earnings depend on network demand, your provider's performance, and the amount you have staked. Better-performing providers with higher stakes tend to earn more. + +### Can I run multiple providers? +Yes, you can run multiple providers. Each provider needs its own unique wallet and must be staked separately. + +### What is the recommended hardware for optimal performance? +While the minimum requirements are 4GB RAM and 2 CPU cores, we recommend at least 8GB RAM and 4 CPU cores for optimal performance. + +### Do I need technical knowledge to run a provider? +Basic familiarity with command line operations and Docker is helpful, but the quickstart guide is designed to be accessible even to those with limited technical experience. + +### How often do I need to update my provider? +Updates will be announced in the community channels. We recommend keeping your provider updated to the latest version for optimal performance and security. + +--- Thank you for contributing to the network's success! diff --git a/orchestrator/package.json b/orchestrator/package.json index 2a04907..051e67b 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^5.4.16", + "ao-process-clients": "^6.0.3", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 91875a3..f6c9c44 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,68 +1,33 @@ import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; -import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient} from "ao-process-clients" -import { dbConfig } from './db_config.js'; -import { getNetworkConfig, launchVDFTask, NetworkConfig } from './ecs_config'; +import { connectWithRetry, dbConfig, setupDatabase } from './db_tools.js'; import Arweave from 'arweave'; +import { cleanupFulfilledEntries, getProviderRequests, getRandomClient, processChallengeRequests, processOutputRequests } from './helperFunctions.js'; +import { checkAndFetchIfNeeded, monitorDockerContainers } from './containerManagment.js'; -const AO_CONFIG = { - MU_URL: "https://ur-mu.randao.net", - CU_URL: "https://ur-cu.randao.net", - GATEWAY_URL: "https://arweave.net", -}; - -let randomClientInstance: RandomClient | null = null; -let lastInitTime: number = 0; -const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds - -async function getRandomClient(): Promise { - const currentTime = Date.now(); - - if (!randomClientInstance || (currentTime - lastInitTime) > REINIT_INTERVAL) { - randomClientInstance = ((await RandomClient.defaultBuilder()) - .withAOConfig(AO_CONFIG)) - .withWallet(JSON.parse(process.env.WALLET_JSON!)) - .build(); - lastInitTime = currentTime; - } - - return randomClientInstance; -} -// Optional: Auto-reinitialize on a timer -setInterval(() => { - randomClientInstance = null; -}, REINIT_INTERVAL); - - -const docker = new Docker(); -// Constants for configuration -const POLLING_INTERVAL_MS = 30000; //30 seconds -const DATABASE_CHECK_TIME = 60000; //60 seconds -const MINIMUM_ENTRIES = 1000; -const DRYRUNTIMEOUT = 30000; // 30 seconds -const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; -const MAX_RETRIES = 10; -const RETRY_DELAY_MS = 10000; -const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; -const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); -const ongoingContainers = new Set(); // Track container IDs of running Docker containers -let PROVIDER_ID = ""; -const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; -const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds -const UNCHAIN_VS_OFFCHAIN_MAX_DIF = 250; +export const docker = new Docker(); +export const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); +export const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; +export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; +export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; -let ongoingRequest = false; -let spotInterruptions = 0; -// Global variables to track polling status +export const DOCKER_MONITORING_TIME= 30000; +export const POLLING_INTERVAL_MS = 30000; //30 seconds +export const DATABASE_CHECK_TIME = 60000; //60 seconds +export const MINIMUM_ENTRIES = 1000; +export const DRYRUNTIMEOUT = 30000; // 30 seconds +export const MAX_RETRIES = 10; +export const RETRY_DELAY_MS = 10000; +export const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds +export const UNCHAIN_VS_OFFCHAIN_MAX_DIF = 250; + +let PROVIDER_ID = ""; let pollingInProgress = false; let lastPollingId: string | null = null; -let pulledDockerimage = false; -// Cache for network configuration -let cachedNetworkConfig: NetworkConfig | null = null; const arweave = Arweave.init({}); @@ -85,249 +50,6 @@ function resetStepTracking() { }; } -// Retry logic for connecting to PostgreSQL -async function connectWithRetry(): Promise { - const client = new Client(dbConfig); - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { - try { - await client.connect(); - console.log(`Connected to PostgreSQL database (Attempt ${attempt})`); - return client; - } catch (error) { - console.error(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); - } - } - throw new Error("Failed to connect to PostgreSQL after multiple attempts"); -} - -// Function to initialize the PostgreSQL database schema -async function setupDatabase(client: Client): Promise { - try { - // Drop old tables maybe - await client.query(` - DROP TABLE IF EXISTS verifiable_delay_functions CASCADE; - `); - - // Create the rsa_keys table - await client.query(` - CREATE TABLE rsa_keys ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - p TEXT NOT NULL, - q TEXT NOT NULL, - modulus TEXT NOT NULL UNIQUE, -- Store hex string of modulus N - phi TEXT NOT NULL - ); - `); - - // Create the time_lock_puzzles table with relation to rsa_keys based on rsa_id - await client.query(` -CREATE TABLE time_lock_puzzles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - x TEXT NOT NULL, - y TEXT NOT NULL, - t TEXT NOT NULL, - modulus TEXT NOT NULL, -- Store hex string of modulus N - request_id TEXT NULL, - rsa_id UUID NOT NULL UNIQUE, - detected_completed TIMESTAMP NULL, -- Added to track completion time - FOREIGN KEY (rsa_id) REFERENCES rsa_keys(id) ON DELETE CASCADE -); - `); - - console.log("✅ Database setup complete. Tables are properly linked on rsa_id."); - } catch (error) { - console.error("❌ Error setting up database:", error); - } -} - - -async function triggerTimePuzzleJobPod(randomCount: number): Promise { - if (ENVIRONMENT === 'cloud') { - try { - console.log("Cloud environment detected. Launching ECS task."); - - // Get or refresh network configuration - if (!cachedNetworkConfig) { - console.log("Fetching network configuration..."); - cachedNetworkConfig = await getNetworkConfig(ecs); - console.log("Network config:", cachedNetworkConfig); - } - - const taskArn = await launchVDFTask(ecs, cachedNetworkConfig, randomCount); - if (taskArn) { - ongoingContainers.add(taskArn); - console.log(`ECS task started successfully: ${taskArn}`); - return taskArn; - } - return null; - } catch (error: any) { - if (error?.code === 'SpotCapacityNotAvailableException') { - spotInterruptions++; - console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } else { - console.error("Error launching ECS task:", error); - // Reset network config cache on error to force refresh on next attempt - cachedNetworkConfig = null; - } - return null; - } - } else { - const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; - console.log(`Starting Docker container with name: ${containerName}`); - try { - if (!pulledDockerimage) { - console.log(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); - await new Promise((resolve, reject) => { - docker.pull(TIME_PUZZLE_JOB_IMAGE, (err: any, stream: NodeJS.ReadableStream) => { - if (err) { - return reject(err); - } - docker.modem.followProgress(stream, (doneErr) => { - if (doneErr) reject(doneErr); - else resolve(true); - }); - }); - }); - pulledDockerimage = true; - } - const container = await docker.createContainer({ - Image: TIME_PUZZLE_JOB_IMAGE, - Cmd: ['sh', '-c', `python3 main.py ${randomCount}`], - - Env: [ - `DATABASE_TYPE=postgresql`, - `DATABASE_HOST=${dbConfig.host}`, - `DATABASE_PORT=${dbConfig.port.toString()}`, - `DATABASE_USER=${dbConfig.user}`, - `DATABASE_PASSWORD=${dbConfig.password}`, - `DATABASE_NAME=${dbConfig.database}`, - ], - HostConfig: { - NetworkMode: DOCKER_NETWORK, - }, - name: containerName - }); - - await container.start(); - ongoingContainers.add(container.id); - console.log(`Docker container ${containerName} started successfully.`); - return container.id; - } catch (error) { - console.error(`Error starting Docker container ${containerName}:`, error); - return null; - } - } -} - - -// Modified function to wait for ECS tasks to complete and remove them from tracking -async function monitorECSTasks(): Promise { - if (ongoingContainers.size === 0) return; - - const describeTasksResult = await ecs.describeTasks({ - cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - tasks: Array.from(ongoingContainers) - }).promise(); - - describeTasksResult.tasks?.forEach(task => { - // Check capacity provider name to determine if it is running Fargate or Fargate Spot - const capacityProvider = task.capacityProviderName || 'Unknown'; - - console.log(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); - - if (task.lastStatus === 'STOPPED') { - console.log(`ECS task stopped: ${task.taskArn}`); - if (task.stoppedReason) { - console.log(`Task stopped reason: ${task.stoppedReason}`); - if (task.stoppedReason.includes('Host EC2 instance termination')) { - spotInterruptions++; - console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } - } else { - console.log('Task stopped due to normal completion.'); - } - ongoingContainers.delete(task.taskArn as string); - } - }); -} - -// Function to wait for Docker containers to complete and remove them from tracking -async function monitorDockerContainers(): Promise { - if (ongoingContainers.size === 0) return; - if (ENVIRONMENT === 'cloud') { - await monitorECSTasks(); - } else { - - for (const containerId of ongoingContainers) { - try { - const container = docker.getContainer(containerId); - const containerInfo = await container.inspect(); - - // Check if the container is already stopped (exited) - if (containerInfo.State.Status === 'exited') { - console.log(`Docker container stopped: ${containerId}`); - - // Attempt to remove the container, handling possible errors gracefully - try { - await container.remove({ force: true }); // Force removal to avoid "in progress" errors - console.log(`Docker container removed: ${containerId}`); - ongoingContainers.delete(containerId); - } catch (removeError) { - if (isDockerError(removeError) && removeError.statusCode === 409) { - // Error 409 means removal is in progress, so skip this container for now - console.log(`Removal of container ${containerId} is already in progress. Skipping.`); - } else { - // Handle other errors that might occur during container removal - console.error(`Error removing Docker container ${containerId}:`, removeError); - } - } - } - } catch (error) { - console.error(`Error inspecting Docker container ${containerId}:`, error); - ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) - } - } - } -} - -// Helper function to type guard Docker errors -function isDockerError(error: unknown): error is { statusCode: number } { - return typeof error === 'object' && error !== null && 'statusCode' in error && typeof (error as any).statusCode === 'number'; -} - -// Function to process hex output for 64-bit modulus -function hexMod64Bit(expectedOutput: string): { expectedOutput64BitBase10: string } { - // Parse the hexadecimal string into a BigInt - const number = BigInt(`${expectedOutput}`); - - // Define the 64-bit modulus (2^64 - 1) - const modulus = BigInt("0x7FFFFFFFF"); - - // Keep dividing by modulus until we get a remainder less than modulus - let remainder = number; - while (remainder >= modulus) { - remainder = remainder % modulus; - } - - // Return the remainder in base 10 - return { - expectedOutput64BitBase10: remainder.toString(), - }; -} - -function updateAvailableValuesAsync(currentCount: number) { - return (async () => { - try { - const randomClient = await getRandomClient(); - await randomClient.updateProviderAvailableValues(currentCount); - console.log(`Updated provider values to ${currentCount}`); - } catch (error) { - console.error("Failed to update provider values:", error); - } - })(); -} - async function shutdown() { try { @@ -341,168 +63,6 @@ async function shutdown() { } -async function getMoreRandom(currentCount: number) { - const entriesNeeded = MINIMUM_ENTRIES - currentCount; - console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); - - ongoingRequest = true; - - if (ongoingContainers.size > 0) { - console.log("A puzzle-gen container is already running. Skipping new container launch."); - return null; - } - - console.log(`Spawning a single container to generate ${entriesNeeded} random values.`); - - try { - const jobId = await triggerTimePuzzleJobPod(entriesNeeded); - if (jobId) { - console.log(`Job triggered: ${jobId}`); - ongoingContainers.add(jobId); - } - } catch (error) { - console.error('Error triggering job pod:', error); - } -} - - - -// Function to check and fetch database entries as needed -async function checkAndFetchIfNeeded(client: Client) { - try { - //Check if provider has been given a special signal - const on_chain_avalible_random = await getProviderAvailableRandomValues(PROVIDER_ID); - // Query current count of usable DB entries - const res = await client.query( - 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' - ); - const currentCount = parseInt(res.rows[0].count, 10); - console.log("Total usable DB entries: " + currentCount); - - switch (on_chain_avalible_random.availibleRandomValues) { - case -1: - console.log("Value is -1"); - console.log("Provider has been shut down by USER..."); - console.log("Go to the provider dashboard to turn back on"); - //TODO prepare for shutdown - //TODO the async causes it to ovewrite itself - break; - case -2: - console.log("Value is -2"); - console.log("Provider has been shut down by PROCESS..."); - console.log("This is due to One of the following: "); - console.log("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); - console.log("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)" ); - console.log("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); - console.log("Go to the provider dashboard to turn back on"); - break; - case -3: - console.log("Value is -3"); - console.log("Provider has been shut down by PROCESS..."); - console.log("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); - console.log("Go to the provider dashboard to turn back on"); - break; - default: - console.log("Value is not -1, -2, or -3"); - console.log("Provider is up and working"); - console.log("Onchain Value is "+ on_chain_avalible_random.availibleRandomValues) - console.log("Local Value is "+ currentCount) - if (Math.abs(on_chain_avalible_random.availibleRandomValues - currentCount) > UNCHAIN_VS_OFFCHAIN_MAX_DIF) { - console.log(`Updating available random values from ${on_chain_avalible_random.availibleRandomValues} to ${currentCount}`); - updateAvailableValuesAsync(currentCount); - } - } - if (ongoingRequest) return; // Prevent redundant operations - - // Check if more entries are needed - if (currentCount >= MINIMUM_ENTRIES) return; - getMoreRandom(currentCount) - - } catch (error) { - console.error('Error during check and fetch:', error); - } finally { - ongoingRequest = false; // Allow future operations - } -} - -// Function to post VDF challenge (fetches dbId dynamically) -async function fulfillRandomChallenge(client: Client, requestId: string, parentLogId: string): Promise { - try { - // Fetch the necessary details from the database using requestId - const res = await client.query( - `SELECT id, modulus, x - FROM time_lock_puzzles - WHERE request_id = $1`, - [requestId] - ); - - if (!res.rowCount) { - console.error(`No entry found for Request ID: ${requestId}`); - return; - } - - const { id: dbId, modulus, x: input } = res.rows[0]; - - console.log(`${parentLogId} Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); - - console.log(`${parentLogId} Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); - await (await getRandomClient()).commit({ - requestId: requestId, - puzzle: { - input: input, - modulus: modulus - } - }); - console.log(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); - } catch (error) { - console.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); - } -} - - - -// Function to post VDF output and proof -async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { - try { - // Fetch the output and proof from the database using the requestId - const res = await client.query( - `SELECT - tlp.id, - tlp.y AS output, - rk.p, - rk.q - FROM time_lock_puzzles tlp - JOIN rsa_keys rk ON tlp.rsa_id = rk.id - WHERE tlp.request_id = $1`, - [requestId] - ); - - if (!res.rowCount) { - console.error(`No entry found for request ID: ${requestId}`); - return; - } - // Map the response to structured variables -const { - id: dbId, - output, // Mapping 'y' to 'output' - p: rsaP, - q: rsaQ -} = res.rows[0]; - console.log(`${parentLogId} Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}, output: ${output}, rsaP: ${rsaP}, rsaQ ${rsaQ} `); - - console.log(`${parentLogId} Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); - await (await getRandomClient()).reveal({ - requestId: requestId, - rsa_key: { - p: rsaP, - q: rsaQ - } - }) - console.log(`${parentLogId} Proof posted for request ID: ${requestId}`); - } catch (error) { - console.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); - } -} function getLogId(): string { const randomId = Math.floor(10000 + Math.random() * 90000); // 5-digit random number @@ -510,81 +70,6 @@ function getLogId(): string { return `[LogID: ${randomId} | ${timestamp}]`; } -async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { - - let openRequests: GetOpenRandomRequestsResponse; - - // Create a function to fetch open requests with a timeout - const fetchOpenRequests = async (): Promise => { - try { - const response = await (await getRandomClient()).getOpenRandomRequests(PROVIDER_ID); - return response; - } catch (error) { - console.error(`${parentLogId} Error fetching requests: ${error}`); - return { /* Return a default or empty response here */ } as GetOpenRandomRequestsResponse; - } - }; - - try { - openRequests = await Promise.race([ - fetchOpenRequests().catch(err => { - throw new Error(`${parentLogId} Fetch Error: ${err}`); - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), DRYRUNTIMEOUT) - ) - ]); - } catch (error) { - console.log(`${parentLogId} Step 1: ${error}`); - //randclient.setDryRunAsMessage(true); - console.log("Removed this as its spamming") - console.log("Switching dryrun off"); - openRequests = await fetchOpenRequests(); // Retry request - } - - if(openRequests.toString().includes("not found")){ - return false - } - console.log(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(openRequests)}`); - console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeChallengeRequests.request_ids.length}`); - console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeOutputRequests.request_ids.length}`); - return openRequests; -} - -async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise { - let avalibleRandom: GetProviderAvailableValuesResponse; - // Create a function to fetch open requests with a timeout - const fetchAvalibleRandom = async (): Promise => { - try { - const response = await (await getRandomClient()).getProviderAvailableValues(PROVIDER_ID); - return response; - } catch (error) { - console.error(`Error fetching avalible random: ${error}`); - return { /* Return a default or empty response here */ } as GetProviderAvailableValuesResponse; - } - }; - - try { - avalibleRandom = await Promise.race([ - fetchAvalibleRandom().catch(err => { - throw new Error(`Fetch Error: ${err}`); - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), DRYRUNTIMEOUT) - ) - ]); - } catch (error) { - // console.log(`${parentLogId} Step 1: ${error}`); - //randclient.setDryRunAsMessage(true); - console.log("Removed this as its spamming") - console.log("Switching dryrun off"); - avalibleRandom = await fetchAvalibleRandom(); // Retry request - } - return avalibleRandom; -} - - - async function polling(client: any) { if (pollingInProgress) { const completedSteps = Object.entries(stepTracking) @@ -636,9 +121,8 @@ async function polling(client: any) { (async () => { const s4 = Date.now(); console.log(`${logId} Step 4 started.`); - //TODO enable this again later - //await cleanupFulfilledEntries(client, openRequests, logId); + await cleanupFulfilledEntries(client, openRequests, logId); stepTracking.step4 = { completed: true, timeTaken: Date.now() - s4 }; console.log(`${logId} Step 4 completed. Time taken: ${stepTracking.step4.timeTaken}ms`); })(), @@ -656,212 +140,6 @@ async function polling(client: any) { -// Step 2: Process Challenge Requests (Database selection & assigning is atomic) -async function processChallengeRequests( - client: Client, - activeChallengeRequests: { request_ids: string[] } | undefined, - parentLogId: string -): Promise { - console.log(`${parentLogId} Step 2: Processing challenge requests.`); - - if (!activeChallengeRequests || activeChallengeRequests.request_ids.length === 0) { - console.log(`${parentLogId} No Challenge Requests to process.`); - return; - } - - const requestIds = activeChallengeRequests.request_ids; - console.log(`${parentLogId} Processing up to ${requestIds.length} requests.`); - - try { - await client.query('BEGIN'); // Start transaction - - console.log(`${parentLogId} Fetching existing request mappings.`); - - // Fetch already assigned request_id -> dbId mappings - const existingMappingsRes = await client.query( - `SELECT request_id FROM time_lock_puzzles - WHERE request_id = ANY($1) - FOR UPDATE SKIP LOCKED`, - [requestIds] - ); - - const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); - console.log(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); - - // Find only the unmapped requests (requestIds not in existingRequestIds) - const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); - console.log(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); - - let mappedEntries: { requestId: string, dbId: number }[] = []; - - if (unmappedRequestIds.length > 0) { - console.log(`${parentLogId} Fetching available DB entries.`); - const dbRes = await client.query( - `SELECT id FROM time_lock_puzzles - WHERE request_id IS NULL - ORDER BY id ASC - LIMIT $1 - FOR UPDATE SKIP LOCKED`, - [unmappedRequestIds.length] - ); - - const availableDbEntries = dbRes.rows.map(row => row.id); - console.log(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); - - if (availableDbEntries.length > 0) { - const numMappings = Math.min(unmappedRequestIds.length, availableDbEntries.length); - - for (let i = 0; i < numMappings; i++) { - await client.query( - `UPDATE time_lock_puzzles - SET request_id = $1 - WHERE id = $2`, - [unmappedRequestIds[i], availableDbEntries[i]] - ); - mappedEntries.push({ requestId: unmappedRequestIds[i], dbId: availableDbEntries[i] }); - console.log(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); - } - } else { - console.log(`${parentLogId} No available DB entries for unmapped requests.`); - } - } - - // Collect all request IDs (previously mapped + newly mapped) - const allRequestIds = [...existingRequestIds, ...mappedEntries.map(entry => entry.requestId)]; - - if (allRequestIds.length === 0) { - console.log(`${parentLogId} No requests to process. Committing transaction.`); - await client.query('COMMIT'); - return; - } - - await client.query('COMMIT'); // Commit all updates at once - console.log(`${parentLogId} Committed all changes. Now fulfilling challenges.`); - - // Call fulfillRandomChallenge for all request IDs - await Promise.all( - allRequestIds.map(requestId => - fulfillRandomChallenge(client, requestId, parentLogId) - .catch(error => console.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) - ) - ); - - console.log(`${parentLogId} All challenges fulfilled`); - } catch (error:any) { - console.error(`${parentLogId} Error in processChallengeRequests:`, error); - await client.query('ROLLBACK'); // Rollback on failure - - console.error(`SQL State: ${error.code}, Message: ${error.message}`); - } - - -} - -// Step 3: Process Output Requests (unchanged but with logging) -async function processOutputRequests( - client: Client, - activeOutputRequests: { request_ids: string[] } | undefined, - parentLogId: string -): Promise { - console.log(`${parentLogId} Step 3: Processing output requests.`); - - if (!activeOutputRequests || activeOutputRequests.request_ids.length === 0) { - console.log(`${parentLogId} No Output Requests to process.`); - return; - } - - const outputPromises = activeOutputRequests.request_ids.map(async (requestId) => { - console.log(`${parentLogId} Processing output request ID: ${requestId}`); - - // Run fulfillRandomOutput asynchronously (do not await) - fulfillRandomOutput(client, requestId, parentLogId) - .catch(error => console.error(`${parentLogId} Error fulfilling output:`, error)); - }); - - await Promise.all(outputPromises); - console.log(`${parentLogId} Step 3 completed.`); -} - -// Step 4: Remove fulfilled entries no longer in use (unchanged but with logging) -async function cleanupFulfilledEntries( - client: Client, - openRequests: any, - parentLogId: string -): Promise { - console.log(`${parentLogId} Step 4: Checking for fulfilled entries no longer in use.`); - - const now = new Date(); - const cutoffTime = new Date(now.getTime() - COMPLETION_RETENTION_PERIOD_MS); - - try { - await client.query('BEGIN'); - - // Fetch all entries with a request_id - const result = await client.query(` - SELECT id, request_id, detected_completed - FROM time_lock_puzzles - WHERE request_id IS NOT NULL - `); - - let markForDeletion: string[] = []; - let markAsCompleted: string[] = []; - - for (const row of result.rows) { - const { id, request_id, detected_completed } = row; - - // Check if this request is still active in challenge or output - const isStillInChallenge = openRequests.activeChallengeRequests?.request_ids.includes(request_id); - const isStillInOutput = openRequests.activeOutputRequests?.request_ids.includes(request_id); - - if (!isStillInChallenge && !isStillInOutput) { - if (!detected_completed) { - // Mark it for deletion by setting detected_completed timestamp - markAsCompleted.push(id); - } else if (new Date(detected_completed) < cutoffTime) { - // If already marked and older than retention period, delete it - markForDeletion.push(id); - } - } - } - - // Mark entries as completed - if (markAsCompleted.length > 0) { - await client.query(` - UPDATE time_lock_puzzles - SET detected_completed = NOW() - WHERE id = ANY($1) - `, [markAsCompleted]); - console.log(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); - } - - // Delete old completed entries //TODO make sure its cleaning up BOTH tables - if (markForDeletion.length > 0) { - await client.query(` - DELETE FROM rsa_keys - WHERE id IN ( - SELECT rsa_id FROM time_lock_puzzles WHERE id = ANY($1) - ); - `, [markForDeletion]); - - await client.query(` - DELETE FROM time_lock_puzzles - WHERE id = ANY($1); - `, [markForDeletion]); - - console.log(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); - } - - await client.query('COMMIT'); - } catch (error) { - console.error(`${parentLogId} Error in cleanupFulfilledEntries:`, error); - await client.query('ROLLBACK'); - } - - console.log(`${parentLogId} Step 4 completed.`); -} - - - // Main function async function run(): Promise { const client = await connectWithRetry(); @@ -870,7 +148,6 @@ async function run(): Promise { arweave.wallets.jwkToAddress(JSON.parse(process.env.WALLET_JSON!)).then((address) => { console.log(address); PROVIDER_ID = address - //1seRanklLU_1VTGkEk7P0xAwMJfA7owA1JHW5KyZKlY }); setInterval(async () => { @@ -878,7 +155,7 @@ async function run(): Promise { console.log(`Periodic log - Current database size: ${res.rows[0].count}`); // Check and fetch entries for the database if needed console.log("Step 0: Checking and fetching database entries if below threshold."); - checkAndFetchIfNeeded(client).catch((error) => { + checkAndFetchIfNeeded(client, PROVIDER_ID).catch((error) => { console.error("Error in checkAndFetchIfNeeded:", error); }); @@ -886,18 +163,12 @@ async function run(): Promise { setInterval(async () => { await monitorDockerContainers(); - }, 30000); // Cleanup every 30 seconds + }, DOCKER_MONITORING_TIME); // Cleanup every 30 seconds setInterval(async () => { await polling(client); }, POLLING_INTERVAL_MS); - // setInterval(async () => { - // randclient.setDryRunAsMessage(false); - // console.log("Switching dryrun on") - // }, DRYRUNRESETTIME); - - process.on("SIGTERM", async () => { console.log("SIGTERM received. Closing database connection."); await client.end(); @@ -907,3 +178,4 @@ async function run(): Promise { } run().catch((err) => console.error(`Error in main function: ${err}`)); + diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts new file mode 100644 index 0000000..438a149 --- /dev/null +++ b/orchestrator/src/containerManagment.ts @@ -0,0 +1,322 @@ +import { docker, DOCKER_NETWORK, ecs, ENVIRONMENT, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; +import AWS from 'aws-sdk'; +import { dbConfig } from "./db_tools"; +import { getProviderAvailableRandomValues, updateAvailableValuesAsync } from "./helperFunctions"; +import { Client } from "pg"; + +export interface NetworkConfig { + subnets: string[]; + securityGroups: string[]; +} + + +let ongoingRequest = false; +let spotInterruptions = 0; +// Global variables to track polling status +let pulledDockerimage = false; +// Cache for network configuration +let cachedNetworkConfig: NetworkConfig | null = null; +const ongoingContainers = new Set(); // Track container IDs of running Docker containers + +export async function getNetworkConfig(ecs: AWS.ECS): Promise { + // Get the task definition to extract network configuration + const taskDef = await ecs.describeTaskDefinition({ taskDefinition: 'vdf-job' }).promise(); + + // Get the service to extract network configuration + const services = await ecs.listServices({ cluster: process.env.ECS_CLUSTER_NAME }).promise(); + const serviceArn = services.serviceArns?.find(arn => arn.includes('vdf-job-service')); + + if (!serviceArn) { + throw new Error('VDF job service not found'); + } + + const service = await ecs.describeServices({ + cluster: process.env.ECS_CLUSTER_NAME, + services: [serviceArn] + }).promise(); + + const networkConfig = service.services?.[0]?.networkConfiguration?.awsvpcConfiguration; + + if (!networkConfig?.subnets || !networkConfig?.securityGroups) { + throw new Error('Network configuration not found'); + } + + return { + subnets: networkConfig.subnets, + securityGroups: networkConfig.securityGroups + }; +} + +export async function launchVDFTask( + ecs: AWS.ECS, + networkConfig: NetworkConfig, + random_per_vdf: number, +): Promise { + const result = await ecs.runTask({ + cluster: process.env.ECS_CLUSTER_NAME, + taskDefinition: 'vdf-job', + capacityProviderStrategy: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 1, + }, + ], + networkConfiguration: { + awsvpcConfiguration: { + subnets: networkConfig.subnets, + securityGroups: networkConfig.securityGroups, + assignPublicIp: 'ENABLED', + }, + }, + count: 1, + overrides: { + containerOverrides: [ + { + name: 'vdf_job_container', // Must match the container name in the task definition + command: ['sh', '-c', `python3 main.py ${random_per_vdf}`], + }, + ], + }, + }).promise(); + + const taskArn = result.tasks?.[0]?.taskArn; + return taskArn || null; +} + + + + +export async function triggerTimePuzzleJobPod(randomCount: number): Promise { + if (ENVIRONMENT === 'cloud') { + try { + console.log("Cloud environment detected. Launching ECS task."); + + // Get or refresh network configuration + if (!cachedNetworkConfig) { + console.log("Fetching network configuration..."); + cachedNetworkConfig = await getNetworkConfig(ecs); + console.log("Network config:", cachedNetworkConfig); + } + + const taskArn = await launchVDFTask(ecs, cachedNetworkConfig, randomCount); + if (taskArn) { + ongoingContainers.add(taskArn); + console.log(`ECS task started successfully: ${taskArn}`); + return taskArn; + } + return null; + } catch (error: any) { + if (error?.code === 'SpotCapacityNotAvailableException') { + spotInterruptions++; + console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); + } else { + console.error("Error launching ECS task:", error); + // Reset network config cache on error to force refresh on next attempt + cachedNetworkConfig = null; + } + return null; + } + } else { + const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + console.log(`Starting Docker container with name: ${containerName}`); + try { + if (!pulledDockerimage) { + console.log(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); + await new Promise((resolve, reject) => { + docker.pull(TIME_PUZZLE_JOB_IMAGE, (err: any, stream: NodeJS.ReadableStream) => { + if (err) { + return reject(err); + } + docker.modem.followProgress(stream, (doneErr) => { + if (doneErr) reject(doneErr); + else resolve(true); + }); + }); + }); + pulledDockerimage = true; + } + const container = await docker.createContainer({ + Image: TIME_PUZZLE_JOB_IMAGE, + Cmd: ['sh', '-c', `python3 main.py ${randomCount}`], + + Env: [ + `DATABASE_TYPE=postgresql`, + `DATABASE_HOST=${dbConfig.host}`, + `DATABASE_PORT=${dbConfig.port.toString()}`, + `DATABASE_USER=${dbConfig.user}`, + `DATABASE_PASSWORD=${dbConfig.password}`, + `DATABASE_NAME=${dbConfig.database}`, + ], + HostConfig: { + NetworkMode: DOCKER_NETWORK, + }, + name: containerName + }); + + await container.start(); + ongoingContainers.add(container.id); + console.log(`Docker container ${containerName} started successfully.`); + return container.id; + } catch (error) { + console.error(`Error starting Docker container ${containerName}:`, error); + return null; + } + } +} + +export async function getMoreRandom(currentCount: number) { + const entriesNeeded = MINIMUM_ENTRIES - currentCount; + console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); + + ongoingRequest = true; + + if (ongoingContainers.size > 0) { + console.log("A puzzle-gen container is already running. Skipping new container launch."); + return null; + } + + console.log(`Spawning a single container to generate ${entriesNeeded} random values.`); + + try { + const jobId = await triggerTimePuzzleJobPod(entriesNeeded); + if (jobId) { + console.log(`Job triggered: ${jobId}`); + ongoingContainers.add(jobId); + } + } catch (error) { + console.error('Error triggering job pod:', error); + } +} + +// Modified function to wait for ECS tasks to complete and remove them from tracking +async function monitorECSTasks(): Promise { + if (ongoingContainers.size === 0) return; + + const describeTasksResult = await ecs.describeTasks({ + cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', + tasks: Array.from(ongoingContainers) + }).promise(); + + describeTasksResult.tasks?.forEach(task => { + // Check capacity provider name to determine if it is running Fargate or Fargate Spot + const capacityProvider = task.capacityProviderName || 'Unknown'; + + console.log(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); + + if (task.lastStatus === 'STOPPED') { + console.log(`ECS task stopped: ${task.taskArn}`); + if (task.stoppedReason) { + console.log(`Task stopped reason: ${task.stoppedReason}`); + if (task.stoppedReason.includes('Host EC2 instance termination')) { + spotInterruptions++; + console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); + } + } else { + console.log('Task stopped due to normal completion.'); + } + ongoingContainers.delete(task.taskArn as string); + } + }); +} + +// Function to wait for Docker containers to complete and remove them from tracking +export async function monitorDockerContainers(): Promise { + if (ongoingContainers.size === 0) return; + if (ENVIRONMENT === 'cloud') { + await monitorECSTasks(); + } else { + + for (const containerId of ongoingContainers) { + try { + const container = docker.getContainer(containerId); + const containerInfo = await container.inspect(); + + // Check if the container is already stopped (exited) + if (containerInfo.State.Status === 'exited') { + console.log(`Docker container stopped: ${containerId}`); + + // Attempt to remove the container, handling possible errors gracefully + try { + await container.remove({ force: true }); // Force removal to avoid "in progress" errors + console.log(`Docker container removed: ${containerId}`); + ongoingContainers.delete(containerId); + } catch (removeError) { + if (isDockerError(removeError) && removeError.statusCode === 409) { + // Error 409 means removal is in progress, so skip this container for now + console.log(`Removal of container ${containerId} is already in progress. Skipping.`); + } else { + // Handle other errors that might occur during container removal + console.error(`Error removing Docker container ${containerId}:`, removeError); + } + } + } + } catch (error) { + console.error(`Error inspecting Docker container ${containerId}:`, error); + ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) + } + } + } +} + +// Helper function to type guard Docker errors +function isDockerError(error: unknown): error is { statusCode: number } { + return typeof error === 'object' && error !== null && 'statusCode' in error && typeof (error as any).statusCode === 'number'; +} + +// Function to check and fetch database entries as needed +export async function checkAndFetchIfNeeded(client: Client, PROVIDER_ID:string) { + try { + //Check if provider has been given a special signal + const on_chain_avalible_random = await getProviderAvailableRandomValues(PROVIDER_ID); + // Query current count of usable DB entries + const res = await client.query( + 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' + ); + const currentCount = parseInt(res.rows[0].count, 10); + console.log("Total usable DB entries: " + currentCount); + + switch (on_chain_avalible_random.availibleRandomValues) { + case -1: + console.log("Value is -1"); + console.log("Provider has been shut down by USER..."); + console.log("Go to the provider dashboard to turn back on"); + //TODO prepare for shutdown + //TODO the async causes it to ovewrite itself + break; + case -2: + console.log("Value is -2"); + console.log("Provider has been shut down by PROCESS..."); + console.log("This is due to One of the following: "); + console.log("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); + console.log("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)" ); + console.log("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); + console.log("Go to the provider dashboard to turn back on"); + break; + case -3: + console.log("Value is -3"); + console.log("Provider has been shut down by PROCESS..."); + console.log("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); + console.log("Go to the provider dashboard to turn back on"); + break; + default: + console.log("Value is not -1, -2, or -3"); + console.log("Provider is up and working"); + console.log("Onchain Value is "+ on_chain_avalible_random.availibleRandomValues) + console.log("Local Value is "+ currentCount) + if (Math.abs(on_chain_avalible_random.availibleRandomValues - currentCount) > UNCHAIN_VS_OFFCHAIN_MAX_DIF) { + console.log(`Updating available random values from ${on_chain_avalible_random.availibleRandomValues} to ${currentCount}`); + updateAvailableValuesAsync(currentCount); + } + } + if (ongoingRequest) return; // Prevent redundant operations + + // Check if more entries are needed + if (currentCount >= MINIMUM_ENTRIES) return; + getMoreRandom(currentCount) + + } catch (error) { + console.error('Error during check and fetch:', error); + } finally { + ongoingRequest = false; // Allow future operations + } +} \ No newline at end of file diff --git a/orchestrator/src/db_config.ts b/orchestrator/src/db_config.ts deleted file mode 100644 index 22c50ca..0000000 --- a/orchestrator/src/db_config.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const dbConfig = { - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432', 10), - user: process.env.DB_USER || 'myuser', - password: process.env.DB_PASSWORD || 'mypassword', - database: process.env.DB_NAME || 'mydatabase', -}; \ No newline at end of file diff --git a/orchestrator/src/db_tools.ts b/orchestrator/src/db_tools.ts new file mode 100644 index 0000000..ec93622 --- /dev/null +++ b/orchestrator/src/db_tools.ts @@ -0,0 +1,80 @@ +import { Client } from "pg"; +import { MAX_RETRIES, RETRY_DELAY_MS } from "./app"; + +export const dbConfig = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + user: process.env.DB_USER || 'myuser', + password: process.env.DB_PASSWORD || 'mypassword', + database: process.env.DB_NAME || 'mydatabase', +}; + +// Clear out the existing database +export async function clearDatabase(client: Client): Promise { + console.log("Clearing database..."); + + await client.query(`SET session_replication_role = 'replica';`); + + const { rows } = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public';`); + for (const row of rows) { + console.log(`Dropping table: ${row.tablename}`); + await client.query(`DROP TABLE IF EXISTS "${row.tablename}" CASCADE;`); + } + + await client.query(`SET session_replication_role = 'origin';`); + + console.log("Database cleared."); +} + +export async function setupDatabase(client: Client): Promise { + try { + // Create rsa_keys table if it doesn't exist + await client.query(` + CREATE TABLE IF NOT EXISTS rsa_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + p TEXT NOT NULL, + q TEXT NOT NULL, + modulus TEXT NOT NULL UNIQUE, + phi TEXT NOT NULL + ); + `); + + // Create time_lock_puzzles table if it doesn't exist + await client.query(` + CREATE TABLE IF NOT EXISTS time_lock_puzzles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + x TEXT NOT NULL, + y TEXT NOT NULL, + t TEXT NOT NULL, + modulus TEXT NOT NULL, + request_id TEXT NULL, + rsa_id UUID NOT NULL UNIQUE, + detected_completed TIMESTAMP NULL, + FOREIGN KEY (rsa_id) REFERENCES rsa_keys(id) ON DELETE CASCADE + ); + `); + + // Drop the old verifiable_delay_functions table if exists + await client.query(`DROP TABLE IF EXISTS verifiable_delay_functions CASCADE;`); + + console.log("✅ Database setup complete or already exists."); + } catch (error: any) { + console.error("❌ Legitimate issue encountered during database setup:", error.message); + } +} + +// Retry logic for connecting to PostgreSQL +export async function connectWithRetry(): Promise { + const client = new Client(dbConfig); + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + await client.connect(); + console.log(`Connected to PostgreSQL database (Attempt ${attempt})`); + return client; + } catch (error) { + console.error(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + } + } + throw new Error("Failed to connect to PostgreSQL after multiple attempts"); +} \ No newline at end of file diff --git a/orchestrator/src/ecs_config.ts b/orchestrator/src/ecs_config.ts deleted file mode 100644 index 5bd863e..0000000 --- a/orchestrator/src/ecs_config.ts +++ /dev/null @@ -1,72 +0,0 @@ -import AWS from 'aws-sdk'; - -export interface NetworkConfig { - subnets: string[]; - securityGroups: string[]; -} - -export async function getNetworkConfig(ecs: AWS.ECS): Promise { - // Get the task definition to extract network configuration - const taskDef = await ecs.describeTaskDefinition({ taskDefinition: 'vdf-job' }).promise(); - - // Get the service to extract network configuration - const services = await ecs.listServices({ cluster: process.env.ECS_CLUSTER_NAME }).promise(); - const serviceArn = services.serviceArns?.find(arn => arn.includes('vdf-job-service')); - - if (!serviceArn) { - throw new Error('VDF job service not found'); - } - - const service = await ecs.describeServices({ - cluster: process.env.ECS_CLUSTER_NAME, - services: [serviceArn] - }).promise(); - - const networkConfig = service.services?.[0]?.networkConfiguration?.awsvpcConfiguration; - - if (!networkConfig?.subnets || !networkConfig?.securityGroups) { - throw new Error('Network configuration not found'); - } - - return { - subnets: networkConfig.subnets, - securityGroups: networkConfig.securityGroups - }; -} - -export async function launchVDFTask( - ecs: AWS.ECS, - networkConfig: NetworkConfig, - random_per_vdf: number, -): Promise { - const result = await ecs.runTask({ - cluster: process.env.ECS_CLUSTER_NAME, - taskDefinition: 'vdf-job', - capacityProviderStrategy: [ - { - capacityProvider: 'FARGATE_SPOT', - weight: 1, - }, - ], - networkConfiguration: { - awsvpcConfiguration: { - subnets: networkConfig.subnets, - securityGroups: networkConfig.securityGroups, - assignPublicIp: 'ENABLED', - }, - }, - count: 1, - overrides: { - containerOverrides: [ - { - name: 'vdf_job_container', // Must match the container name in the task definition - command: ['sh', '-c', `python3 main.py ${random_per_vdf}`], - }, - ], - }, - }).promise(); - - const taskArn = result.tasks?.[0]?.taskArn; - return taskArn || null; -} - diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts new file mode 100644 index 0000000..b1f26c8 --- /dev/null +++ b/orchestrator/src/helperFunctions.ts @@ -0,0 +1,396 @@ +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, Logger, LogLevel, RandomClient } from "ao-process-clients"; +import { Client } from "pg"; +import { COMPLETION_RETENTION_PERIOD_MS, DRYRUNTIMEOUT } from "./app"; + + +let randomClientInstance: RandomClient | null = null; +let lastInitTime: number = 0; +const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds + +const AO_CONFIG = { + MU_URL: "https://ur-mu.randao.net", + CU_URL: "https://ur-cu.randao.net", + GATEWAY_URL: "https://arweave.net", +}; +// Optional: Auto-reinitialize on a timer +setInterval(() => { + randomClientInstance = null; +}, REINIT_INTERVAL); + +export async function getRandomClient(): Promise { + const currentTime = Date.now(); + Logger.setLogLevel(LogLevel.DEBUG) + if (!randomClientInstance || (currentTime - lastInitTime) > REINIT_INTERVAL) { + randomClientInstance = ((await RandomClient.defaultBuilder()) + .withAOConfig(AO_CONFIG)) + .withWallet(JSON.parse(process.env.WALLET_JSON!)) + .build(); + lastInitTime = currentTime; + } + + return randomClientInstance; +} + + + + + + + + +// Step 2: Process Challenge Requests (Database selection & assigning is atomic) +export async function processChallengeRequests( + client: Client, + activeChallengeRequests: { request_ids: string[] } | undefined, + parentLogId: string +): Promise { + console.log(`${parentLogId} Step 2: Processing challenge requests.`); + + if (!activeChallengeRequests || activeChallengeRequests.request_ids.length === 0) { + console.log(`${parentLogId} No Challenge Requests to process.`); + return; + } + + const requestIds = activeChallengeRequests.request_ids; + console.log(`${parentLogId} Processing up to ${requestIds.length} requests.`); + + try { + await client.query('BEGIN'); // Start transaction + + console.log(`${parentLogId} Fetching existing request mappings.`); + + // Fetch already assigned request_id -> dbId mappings + const existingMappingsRes = await client.query( + `SELECT request_id FROM time_lock_puzzles + WHERE request_id = ANY($1) + FOR UPDATE SKIP LOCKED`, + [requestIds] + ); + + const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); + console.log(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); + + // Find only the unmapped requests (requestIds not in existingRequestIds) + const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); + console.log(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); + + let mappedEntries: { requestId: string, dbId: number }[] = []; + + if (unmappedRequestIds.length > 0) { + console.log(`${parentLogId} Fetching available DB entries.`); + const dbRes = await client.query( + `SELECT id FROM time_lock_puzzles + WHERE request_id IS NULL + ORDER BY id ASC + LIMIT $1 + FOR UPDATE SKIP LOCKED`, + [unmappedRequestIds.length] + ); + + const availableDbEntries = dbRes.rows.map(row => row.id); + console.log(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); + + if (availableDbEntries.length > 0) { + const numMappings = Math.min(unmappedRequestIds.length, availableDbEntries.length); + + for (let i = 0; i < numMappings; i++) { + await client.query( + `UPDATE time_lock_puzzles + SET request_id = $1 + WHERE id = $2`, + [unmappedRequestIds[i], availableDbEntries[i]] + ); + mappedEntries.push({ requestId: unmappedRequestIds[i], dbId: availableDbEntries[i] }); + console.log(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); + } + } else { + console.log(`${parentLogId} No available DB entries for unmapped requests.`); + } + } + + // Collect all request IDs (previously mapped + newly mapped) + const allRequestIds = [...existingRequestIds, ...mappedEntries.map(entry => entry.requestId)]; + + if (allRequestIds.length === 0) { + console.log(`${parentLogId} No requests to process. Committing transaction.`); + await client.query('COMMIT'); + return; + } + + await client.query('COMMIT'); // Commit all updates at once + console.log(`${parentLogId} Committed all changes. Now fulfilling challenges.`); + + // Call fulfillRandomChallenge for all request IDs + await Promise.all( + allRequestIds.map(requestId => + fulfillRandomChallenge(client, requestId, parentLogId) + .catch(error => console.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) + ) + ); + + console.log(`${parentLogId} All challenges fulfilled`); + } catch (error:any) { + console.error(`${parentLogId} Error in processChallengeRequests:`, error); + await client.query('ROLLBACK'); // Rollback on failure + + console.error(`SQL State: ${error.code}, Message: ${error.message}`); + } +} +// Step 3: Process Output Requests (unchanged but with logging) +export async function processOutputRequests( + client: Client, + activeOutputRequests: { request_ids: string[] } | undefined, + parentLogId: string +): Promise { + console.log(`${parentLogId} Step 3: Processing output requests.`); + + if (!activeOutputRequests || activeOutputRequests.request_ids.length === 0) { + console.log(`${parentLogId} No Output Requests to process.`); + return; + } + + const outputPromises = activeOutputRequests.request_ids.map(async (requestId) => { + console.log(`${parentLogId} Processing output request ID: ${requestId}`); + + // Run fulfillRandomOutput asynchronously (do not await) + fulfillRandomOutput(client, requestId, parentLogId) + .catch(error => console.error(`${parentLogId} Error fulfilling output:`, error)); + }); + + await Promise.all(outputPromises); + console.log(`${parentLogId} Step 3 completed.`); +} + +// Step 4: Remove fulfilled entries no longer in use (unchanged but with logging) +export async function cleanupFulfilledEntries( + client: Client, + openRequests: any, + parentLogId: string +): Promise { + console.log(`${parentLogId} Step 4: Checking for fulfilled entries no longer in use.`); + + const now = new Date(); + const cutoffTime = new Date(now.getTime() - COMPLETION_RETENTION_PERIOD_MS); + + try { + await client.query('BEGIN'); + + // Fetch all entries with a request_id + const result = await client.query(` + SELECT id, request_id, detected_completed + FROM time_lock_puzzles + WHERE request_id IS NOT NULL + `); + + let markForDeletion: string[] = []; + let markAsCompleted: string[] = []; + + for (const row of result.rows) { + const { id, request_id, detected_completed } = row; + + // Check if this request is still active in challenge or output + const isStillInChallenge = openRequests.activeChallengeRequests?.request_ids.includes(request_id); + const isStillInOutput = openRequests.activeOutputRequests?.request_ids.includes(request_id); + + if (!isStillInChallenge && !isStillInOutput) { + if (!detected_completed) { + // Mark it for deletion by setting detected_completed timestamp + markAsCompleted.push(id); + } else if (new Date(detected_completed) < cutoffTime) { + // If already marked and older than retention period, delete it + markForDeletion.push(id); + } + } + } + + // Mark entries as completed + if (markAsCompleted.length > 0) { + await client.query(` + UPDATE time_lock_puzzles + SET detected_completed = NOW() + WHERE id = ANY($1) + `, [markAsCompleted]); + console.log(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); + } + + // Delete old completed entries //TODO make sure its cleaning up BOTH tables + if (markForDeletion.length > 0) { + await client.query(` + DELETE FROM rsa_keys + WHERE id IN ( + SELECT rsa_id FROM time_lock_puzzles WHERE id = ANY($1) + ); + `, [markForDeletion]); + + await client.query(` + DELETE FROM time_lock_puzzles + WHERE id = ANY($1); + `, [markForDeletion]); + + console.log(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); + } + + await client.query('COMMIT'); + } catch (error) { + console.error(`${parentLogId} Error in cleanupFulfilledEntries:`, error); + await client.query('ROLLBACK'); + } + + console.log(`${parentLogId} Step 4 completed.`); +} + + + + + + + + + + + + + + + + + + + +export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { + + let openRequests: GetOpenRandomRequestsResponse; + + // Create a function to fetch open requests with a timeout + const fetchOpenRequests = async (): Promise => { + try { + const response = await (await getRandomClient()).getOpenRandomRequests(PROVIDER_ID); + return response; + } catch (error) { + console.error(`${parentLogId} Error fetching requests: ${error}`); + return { /* Return a default or empty response here */ } as GetOpenRandomRequestsResponse; + } + }; + + try { + openRequests = await Promise.race([ + fetchOpenRequests().catch(err => { + throw new Error(`${parentLogId} Fetch Error: ${err}`); + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), DRYRUNTIMEOUT) + ) + ]); + } catch (error) { + console.log(`${parentLogId} Step 1: ${error}`); + //randclient.setDryRunAsMessage(true); + console.log("Removed this as its spamming") + console.log("Switching dryrun off"); + openRequests = await fetchOpenRequests(); // Retry request + } + + if(openRequests.toString().includes("not found")){ + return false + } + console.log(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(openRequests)}`); + console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeChallengeRequests.request_ids.length}`); + console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeOutputRequests.request_ids.length}`); + return openRequests; +} + +export function updateAvailableValuesAsync(currentCount: number) { + return (async () => { + try { + const randomClient = await getRandomClient(); + await randomClient.updateProviderAvailableValues(currentCount); + console.log(`Updated provider values to ${currentCount}`); + } catch (error) { + console.error("Failed to update provider values:", error); + } + })(); +} +export async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise { + try { + const randomClient = await getRandomClient(); + return await randomClient.getProviderAvailableValues(PROVIDER_ID); + } catch (error) { + console.error(`Error fetching available random values: ${error}`); + return {} as GetProviderAvailableValuesResponse; + } +} + +// Function to post VDF challenge (fetches dbId dynamically) +async function fulfillRandomChallenge(client: Client, requestId: string, parentLogId: string): Promise { + try { + // Fetch the necessary details from the database using requestId + const res = await client.query( + `SELECT id, modulus, x + FROM time_lock_puzzles + WHERE request_id = $1`, + [requestId] + ); + + if (!res.rowCount) { + console.error(`No entry found for Request ID: ${requestId}`); + return; + } + + const { id: dbId, modulus, x: input } = res.rows[0]; + + console.log(`${parentLogId} Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); + + console.log(`${parentLogId} Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); + await (await getRandomClient()).commit({ + requestId: requestId, + puzzle: { + input: input, + modulus: modulus + } + }); + console.log(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); + } catch (error) { + console.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); + } +} +// Function to post VDF output and proof +async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { + try { + // Fetch the output and proof from the database using the requestId + const res = await client.query( + `SELECT + tlp.id, + tlp.y AS output, + rk.p, + rk.q + FROM time_lock_puzzles tlp + JOIN rsa_keys rk ON tlp.rsa_id = rk.id + WHERE tlp.request_id = $1`, + [requestId] + ); + + if (!res.rowCount) { + console.error(`No entry found for request ID: ${requestId}`); + return; + } + // Map the response to structured variables +const { + id: dbId, + output, // Mapping 'y' to 'output' + p: rsaP, + q: rsaQ +} = res.rows[0]; + console.log(`${parentLogId} Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}, output: ${output}, rsaP: ${rsaP}, rsaQ ${rsaQ} `); + + console.log(`${parentLogId} Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); + await (await getRandomClient()).reveal({ + requestId: requestId, + rsa_key: { + p: rsaP, + q: rsaQ + } + }) + console.log(`${parentLogId} Proof posted for request ID: ${requestId}`); + } catch (error) { + console.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); + } +} \ No newline at end of file diff --git a/orchestrator/src/clear_outputs.tzs b/orchestrator/src/oldjunk/clear_outputs.tzs similarity index 100% rename from orchestrator/src/clear_outputs.tzs rename to orchestrator/src/oldjunk/clear_outputs.tzs diff --git a/orchestrator/src/stake.tzs b/orchestrator/src/oldjunk/stake.tzs similarity index 100% rename from orchestrator/src/stake.tzs rename to orchestrator/src/oldjunk/stake.tzs diff --git a/orchestrator/src/reset_db.ts b/orchestrator/src/reset_db.ts index ae62d50..8a58e79 100644 --- a/orchestrator/src/reset_db.ts +++ b/orchestrator/src/reset_db.ts @@ -1,5 +1,5 @@ -import { Client, QueryResult } from 'pg'; -import { dbConfig } from './db_config'; +import { Client } from 'pg'; +import { dbConfig } from './db_tools'; interface TableRow { tablename: string; diff --git a/orchestrator/tsconfig.json b/orchestrator/tsconfig.json index 786e06e..bb6eb3a 100644 --- a/orchestrator/tsconfig.json +++ b/orchestrator/tsconfig.json @@ -14,7 +14,7 @@ }, "include": [ "src/**/*.ts", - "src/db_config.ts", + "src/db_tools.ts", "src/clear_all_output_requests.ts", "src/reset_db.mjs", "src/clear_outputs.tzs" diff --git a/requester/src/app.ts b/requester/src/app.ts index 5837991..047de7f 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -1,15 +1,8 @@ import { - ProviderStakingClient, - getRandomClientAutoConfiguration, - IRandomClient, RandomClient, - RandomClientConfig, - StakingClient, - ProviderProfileClient, - RandomClientConfigBuilder, } from "ao-process-clients"; -const RETRY_DELAY_MS = 30000; // 30 seconds +const RETRY_DELAY_MS = 5000; // 5 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; diff --git a/terraform/.debug_task.tf b/terraform/.debug_task.tf deleted file mode 100644 index 492af95..0000000 --- a/terraform/.debug_task.tf +++ /dev/null @@ -1,36 +0,0 @@ -# debug_task.tf - -resource "aws_ecs_task_definition" "debug_task" { - family = "debug-task" - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - cpu = "512" - memory = "1024" - - container_definitions = jsonencode([ - { - name = "debug-container" - image = "amazonlinux" - essential = true - command = ["/bin/sh", "-c", "while true; do sleep 60; done"] - environment = [ - { name = "DB_HOST", value = "postgres" }, - { name = "DB_PORT", value = "5432" }, - { name = "DB_USER", value = "myuser" }, - { name = "DB_PASSWORD", value = "mypassword" }, - { name = "DB_NAME", value = "mydatabase" } - ], - logConfiguration = { - logDriver = "awslogs" - options = { - awslogs-group = "/ecs/debug-task" - awslogs-region = var.aws_region - awslogs-stream-prefix = "debug-container" - } - } - } - ]) - - execution_role_arn = aws_iam_role.execution_role.arn - task_role_arn = aws_iam_role.task_role.arn -} diff --git a/terraform/README.md b/terraform/README.md deleted file mode 100644 index 931ab97..0000000 --- a/terraform/README.md +++ /dev/null @@ -1,184 +0,0 @@ -# AWS Terraform Setup Guide - -This guide walks you through deploying a randomness provider using Terraform and AWS cloud services for optimal performance and reliability. - -## Prerequisites - -- An AWS account with appropriate permissions -- Basic familiarity with AWS services and Terraform -- Terraform installed on your local machine - -## Steps to Deploy - -1. **Install Terraform** - Follow the [official Terraform installation guide](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli). - -2. **Set Up IAM User and Policy** - 1. Go to AWS IAM and create a new policy called `RandAO-Provider-Admin` - 2. Select the JSON tab and paste this policy: - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "ecs:CreateCluster", - "ecs:DeleteCluster", - "ecs:CreateService", - "ecs:DeleteService", - "ecs:UpdateService", - "ecs:RegisterTaskDefinition", - "ecs:DeregisterTaskDefinition", - "ecs:ListTaskDefinitions", - "ecs:DescribeTaskDefinition", - "ecs:PutClusterCapacityProviders", - "ecs:DescribeClusters" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "iam:CreateRole", - "iam:DeleteRole", - "iam:GetRole", - "iam:PutRolePolicy", - "iam:DeleteRolePolicy", - "iam:AttachRolePolicy", - "iam:DetachRolePolicy", - "iam:PassRole" - ], - "Resource": "arn:aws:iam::*:role/orchestrator-*" - }, - { - "Effect": "Allow", - "Action": [ - "secretsmanager:CreateSecret", - "secretsmanager:DeleteSecret", - "secretsmanager:GetSecretValue", - "secretsmanager:PutSecretValue", - "secretsmanager:UpdateSecret", - "secretsmanager:TagResource" - ], - "Resource": "arn:aws:secretsmanager:*:*:secret:/orchestrator/*" - }, - { - "Effect": "Allow", - "Action": [ - "rds:CreateDBInstance", - "rds:DeleteDBInstance", - "rds:ModifyDBInstance", - "rds:DescribeDBInstances", - "rds:CreateDBSubnetGroup", - "rds:DeleteDBSubnetGroup", - "rds:ModifyDBSubnetGroup", - "rds:AddTagsToResource" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:DeleteLogGroup", - "logs:PutRetentionPolicy" - ], - "Resource": "arn:aws:logs:*:*:log-group:*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateSecurityGroup", - "ec2:DeleteSecurityGroup", - "ec2:AuthorizeSecurityGroupIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:CreateVpcEndpoint", - "ec2:DeleteVpcEndpoints", - "ec2:DescribeVpcEndpoints", - "ec2:DescribeSecurityGroups", - "ec2:DescribeNetworkInterfaces", - "ec2:CreateTags" - ], - "Resource": "*" - } - ] - } - ``` - 3. Save the policy and name it - 4. Go to Users and create a new user called `TerraformDeployer` - 5. Attach the `RandAO-Provider-Admin` policy directly - 6. Create an access key for the user (choose CLI) - 7. Save the access key ID and secret access key for the next step - -3. **Configure AWS Environment Variables** - Open a terminal and enter the following commands: - ```bash - export AWS_ACCESS_KEY_ID="your-access-key-id" - export AWS_SECRET_ACCESS_KEY="your-secret-access-key" - export AWS_REGION="your-region" # e.g., us-east-1 - ``` - -4. **Set Up Terraform Variables** - Navigate to the Terraform directory of the project: - ```bash - cp terraform.tfvars.example terraform.tfvars - ``` - Edit the `terraform.tfvars` file and fill in all required variables: - - **Required Variables:** - - `provider_id`: Your unique provider identifier - - `local_db_user`: Database username - - `local_db_password`: Database password - - `local_wallet_json`: Wallet JSON (either direct or via file path) - - **Optional Variables (with defaults):** - - `aws_region`: AWS region (default: us-east-1) - - `db_name`: Database name (default: orchestrator_db) - - `secrets_prefix`: Prefix for secrets (default: /orchestrator) - - The database configuration is highly suggested, and the secrets can be left at default values as they're just the names of the secrets. - -5. **Initialize and Apply Terraform Configuration** - Navigate to the Terraform directory of the project and run: - ```bash - terraform init - terraform apply - ``` - Type `yes` when prompted to confirm. - -6. **Verify Deployment** - Open the AWS console and navigate to the ECS console to view your running services. - -## How It Works - -This deployment creates the following AWS resources: - -1. **ECS Cluster**: Runs the orchestrator service and puzzle generator jobs -2. **RDS**: PostgreSQL database for storing time lock puzzles and provider state -3. **Secrets Manager**: Securely stores database credentials and wallet information -4. **IAM**: Roles and policies for ECS tasks and secrets access -5. **CloudWatch**: Log groups for monitoring your provider - -## Advantages of AWS Deployment - -- **Cost Efficiency at Scale**: Most cost-effective for dedicated providers -- **Maximum Reliability**: AWS services offer SLAs for high availability -- **Automatic Scaling**: Resources scale based on demand -- **Managed Services**: AWS handles infrastructure maintenance -- **High Performance**: Optimized for speed and reliability - -## Security Notes - -- The `terraform.tfvars` file contains sensitive information and should never be committed to version control -- Add `terraform.tfvars` and `wallet.json` to your `.gitignore` -- In production, sensitive values are stored in AWS Secrets Manager -- Local development variables are only used for initial setup and testing - -## Staking Process - -After deployment, please open the AWS console and share the logs with Ethan to receive the tokens needed for staking. Follow the main documentation for the staking process. - ---- - -[Return to Main Documentation](../README.md) diff --git a/terraform/ecs.tf b/terraform/ecs.tf deleted file mode 100644 index ca10463..0000000 --- a/terraform/ecs.tf +++ /dev/null @@ -1,315 +0,0 @@ -# ECS Cluster and Services -resource "aws_ecs_cluster" "fargate_cluster" { - name = "orchestrator-cluster" -} - -resource "aws_ecs_cluster_capacity_providers" "fargate_providers" { - cluster_name = aws_ecs_cluster.fargate_cluster.name - - capacity_providers = ["FARGATE", "FARGATE_SPOT"] - - default_capacity_provider_strategy { - base = 1 - weight = 100 - capacity_provider = "FARGATE_SPOT" - } -} - -# Orchestrator Service -resource "aws_ecs_service" "orchestrator_service" { - name = "orchestrator-service" - cluster = aws_ecs_cluster.fargate_cluster.id - task_definition = aws_ecs_task_definition.orchestrator_service.arn - desired_count = 1 - launch_type = "FARGATE" - - network_configuration { - subnets = local.subnet_ids - security_groups = local.ecs_security_groups - assign_public_ip = true - } -} - -# VDF Job Service -resource "aws_ecs_service" "vdf_job_service" { - name = "vdf-job-service" - cluster = aws_ecs_cluster.fargate_cluster.id - task_definition = aws_ecs_task_definition.vdf_job.arn - desired_count = 0 - - capacity_provider_strategy { - capacity_provider = "FARGATE_SPOT" - weight = 1 - } - - network_configuration { - subnets = local.subnet_ids - security_groups = local.ecs_security_groups - assign_public_ip = true - } -} - -# Task Definitions -resource "aws_ecs_task_definition" "orchestrator_service" { - family = "orchestrator-service" - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - cpu = "512" - memory = "1024" - execution_role_arn = aws_iam_role.ecs_execution_role.arn - task_role_arn = aws_iam_role.ecs_task_role.arn - - container_definitions = jsonencode([ - { - name = "orchestrator" - image = "randao/orchestrator:v0.4.55" - environment = [ - { - name = "ENVIRONMENT" - value = "cloud" - }, - { - name = "PROVIDER_ID" - value = var.provider_id - }, - { - name = "DB_HOST" - value = split(":", aws_db_instance.orchestrator.endpoint)[0] - }, - { - name = "DB_PORT" - value = "5432" - }, - { - name = "DB_NAME" - value = var.db_name - }, - { - name = "MAX_RETRIES" - value = "10" - }, - { - name = "RETRY_DELAY_MS" - value = "10000" - }, - { - name = "PGCONNECT_TIMEOUT" - value = "30" - }, - { - name = "AWS_REGION" - value = var.aws_region - }, - { - name = "ECS_CLUSTER_NAME" - value = aws_ecs_cluster.fargate_cluster.name - } - ] - secrets = [ - { - name = "DB_USER" - valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:username::" - }, - { - name = "DB_PASSWORD" - valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:password::" - }, - { - name = "WALLET_JSON" - valueFrom = aws_secretsmanager_secret.wallet.arn - } - ] - logConfiguration = { - logDriver = "awslogs" - options = { - awslogs-group = "/ecs/orchestrator" - awslogs-region = var.aws_region - awslogs-stream-prefix = "orchestrator" - } - } - } - ]) -} - -# VDF Task Definition -resource "aws_ecs_task_definition" "vdf_job" { - family = "vdf-job" - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - cpu = "2048" # 2 vCPU - memory = "4096" # 4GB - execution_role_arn = aws_iam_role.ecs_execution_role.arn - task_role_arn = aws_iam_role.ecs_task_role.arn - - container_definitions = jsonencode([ - { - name = "vdf_job_container" - image = "randao/puzzle-gen:v0.1.1" - command = ["python", "main.py"], - environment = [ - { - name = "DATABASE_TYPE" - value = "postgresql" - }, - { - name = "DATABASE_HOST" - value = split(":", aws_db_instance.orchestrator.endpoint)[0] - }, - { - name = "DATABASE_PORT" - value = "5432" - }, - { - name = "DATABASE_NAME" - value = var.db_name - } - ] - secrets = [ - { - name = "DATABASE_USER" - valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:username::" - }, - { - name = "DATABASE_PASSWORD" - valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:password::" - } - ] - logConfiguration = { - logDriver = "awslogs" - options = { - awslogs-group = "/ecs/vdf-job" - awslogs-region = var.aws_region - awslogs-stream-prefix = "vdf" - } - } - } - ]) -} - -# IAM Roles -resource "aws_iam_role" "ecs_execution_role" { - name = "orchestrator-ecs-execution-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - } - ] - }) -} - -resource "aws_iam_role" "ecs_task_role" { - name = "orchestrator-ecs-task-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - } - ] - }) -} - -# Attach AWS managed policy for ECS task execution -resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" { - role = aws_iam_role.ecs_execution_role.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" -} - -# Add ECS permissions to task role -resource "aws_iam_role_policy" "ecs_task_permissions" { - name = "orchestrator-ecs-permissions" - role = aws_iam_role.ecs_task_role.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "ecs:RunTask", - "ecs:StopTask", - "ecs:DescribeTasks", - "ecs:ListTasks", - "ecs:DescribeTaskDefinition", - "ecs:ListTaskDefinitions", - "ecs:DescribeServices", - "ecs:ListServices", - "ecs:DescribeClusters", - "ecs:ListClusters", - "iam:PassRole" - ] - Resource = "*" - } - ] - }) -} - -# Add networking permissions to task role -resource "aws_iam_role_policy" "ecs_task_networking_policy" { - name = "orchestrator-networking-policy" - role = aws_iam_role.ecs_task_role.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "ec2:DescribeNetworkInterfaces", - "ec2:CreateNetworkInterface", - "ec2:DeleteNetworkInterface", - "ec2:DescribeInstances", - "ec2:AttachNetworkInterface" - ] - Resource = ["*"] - } - ] - }) -} - -# Add Secrets Manager access to execution role -resource "aws_iam_role_policy" "ecs_execution_secrets_policy" { - name = "orchestrator-secrets-access" - role = aws_iam_role.ecs_execution_role.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "secretsmanager:GetSecretValue", - "secretsmanager:DescribeSecret", - "kms:Decrypt" - ] - Resource = [ - aws_secretsmanager_secret.db_credentials.arn, - aws_secretsmanager_secret.wallet.arn - ] - } - ] - }) -} - -# CloudWatch Log Groups -resource "aws_cloudwatch_log_group" "orchestrator" { - name = "/ecs/orchestrator" - retention_in_days = 30 -} - -resource "aws_cloudwatch_log_group" "vdf" { - name = "/ecs/vdf-job" - retention_in_days = 30 -} diff --git a/terraform/iam.json b/terraform/iam.json deleted file mode 100644 index 1150854..0000000 --- a/terraform/iam.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "ecs:CreateCluster", - "ecs:DeleteCluster", - "ecs:CreateService", - "ecs:DeleteService", - "ecs:UpdateService", - "ecs:RegisterTaskDefinition", - "ecs:DeregisterTaskDefinition", - "ecs:ListTaskDefinitions", - "ecs:DescribeTaskDefinition", - "ecs:PutClusterCapacityProviders", - "ecs:DescribeClusters" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "iam:CreateRole", - "iam:DeleteRole", - "iam:GetRole", - "iam:PutRolePolicy", - "iam:DeleteRolePolicy", - "iam:AttachRolePolicy", - "iam:DetachRolePolicy", - "iam:PassRole" - ], - "Resource": "arn:aws:iam::*:role/orchestrator-*" - }, - { - "Effect": "Allow", - "Action": [ - "secretsmanager:CreateSecret", - "secretsmanager:DeleteSecret", - "secretsmanager:GetSecretValue", - "secretsmanager:PutSecretValue", - "secretsmanager:UpdateSecret", - "secretsmanager:TagResource" - ], - "Resource": "arn:aws:secretsmanager:*:*:secret:/orchestrator/*" - }, - { - "Effect": "Allow", - "Action": [ - "rds:CreateDBInstance", - "rds:DeleteDBInstance", - "rds:ModifyDBInstance", - "rds:DescribeDBInstances", - "rds:CreateDBSubnetGroup", - "rds:DeleteDBSubnetGroup", - "rds:ModifyDBSubnetGroup", - "rds:AddTagsToResource" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:DeleteLogGroup", - "logs:PutRetentionPolicy" - ], - "Resource": "arn:aws:logs:*:*:log-group:*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateSecurityGroup", - "ec2:DeleteSecurityGroup", - "ec2:AuthorizeSecurityGroupIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:CreateVpcEndpoint", - "ec2:DeleteVpcEndpoints", - "ec2:DescribeVpcEndpoints", - "ec2:DescribeSecurityGroups", - "ec2:DescribeNetworkInterfaces", - "ec2:CreateTags" - ], - "Resource": "*" - } - ] -} diff --git a/terraform/iam_roles.tf b/terraform/iam_roles.tf deleted file mode 100644 index 8569ed1..0000000 --- a/terraform/iam_roles.tf +++ /dev/null @@ -1,83 +0,0 @@ -# iam_roles.tf - -data "aws_caller_identity" "current" {} - -resource "aws_iam_role" "task_role" { - name = "orchestrator-task-role" - assume_role_policy = data.aws_iam_policy_document.task_role_policy.json -} - -resource "aws_iam_role" "execution_role" { - name = "orchestrator-execution-role" - assume_role_policy = data.aws_iam_policy_document.execution_role_policy.json -} - -data "aws_iam_policy_document" "task_role_policy" { - statement { - actions = ["sts:AssumeRole"] - principals { - type = "Service" - identifiers = ["ecs-tasks.amazonaws.com"] - } - } -} - -data "aws_iam_policy_document" "execution_role_policy" { - statement { - actions = ["sts:AssumeRole"] - principals { - type = "Service" - identifiers = ["ecs-tasks.amazonaws.com"] - } - } -} - -# Attach necessary policies to orchestrator-task-role -resource "aws_iam_role_policy" "orchestrator_task_policy" { - name = "orchestrator-task-policy" - role = aws_iam_role.task_role.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "ecs:RunTask", - "ecs:StopTask", - "ecs:DescribeTasks", - "ecs:ListTasks", - "ecs:DescribeTaskDefinition", - "ecs:ListTaskDefinitions", - "ecs:DescribeServices", - "ecs:ListServices", - "ecs:DescribeClusters", - "ecs:ListClusters" - ] - Resource = "*" - }, - { - Effect = "Allow" - Action = [ - "iam:PassRole" - ] - Resource = [ - aws_iam_role.execution_role.arn, - aws_iam_role.task_role.arn - ] - } - ] - }) -} - - -# Attach policies to the execution role -resource "aws_iam_role_policy_attachment" "ecs_task_execution" { - role = aws_iam_role.execution_role.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" -} - -resource "aws_iam_role_policy_attachment" "ecs_logs_policy" { - role = aws_iam_role.execution_role.name - policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" -} diff --git a/terraform/locals.tf b/terraform/locals.tf deleted file mode 100644 index f83cd52..0000000 --- a/terraform/locals.tf +++ /dev/null @@ -1,23 +0,0 @@ -# Local variables - -locals { - # Default VPC subnets - you may want to customize these for your environment - subnet_ids = data.aws_subnets.default.ids -} - -# Data sources for networking -data "aws_vpc" "default" { - default = true -} - -data "aws_subnets" "default" { - filter { - name = "vpc-id" - values = [data.aws_vpc.default.id] - } -} - -data "aws_security_group" "default" { - vpc_id = data.aws_vpc.default.id - name = "default" -} diff --git a/terraform/logging.tf b/terraform/logging.tf deleted file mode 100644 index 5ee8731..0000000 --- a/terraform/logging.tf +++ /dev/null @@ -1,6 +0,0 @@ -# logging.tf - -resource "aws_cloudwatch_log_group" "ecs_orchestrator_service" { - name = "ecs-orchestrator-service" - retention_in_days = 30 # Adjust retention as needed -} diff --git a/terraform/outputs.tf b/terraform/outputs.tf deleted file mode 100644 index aedeac0..0000000 --- a/terraform/outputs.tf +++ /dev/null @@ -1,18 +0,0 @@ -# outputs.tf - -# Outputs for resource information - -output "database_endpoint" { - description = "The connection endpoint for the RDS instance" - value = aws_db_instance.orchestrator.endpoint -} - -output "orchestrator_service_name" { - description = "The name of the ECS service running the orchestrator" - value = aws_ecs_service.orchestrator_service.name -} - -output "cloudwatch_log_group" { - description = "The CloudWatch log group for the orchestrator service" - value = aws_cloudwatch_log_group.orchestrator.name -} diff --git a/terraform/package.json b/terraform/package.json deleted file mode 100644 index b4f6756..0000000 --- a/terraform/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "ao-process-clients": "^2.3.17" - }, - "devDependencies": { - "@types/node": "^22.9.1" - } -} diff --git a/terraform/providers.tf b/terraform/providers.tf deleted file mode 100644 index c40a6fd..0000000 --- a/terraform/providers.tf +++ /dev/null @@ -1,16 +0,0 @@ -# Configure AWS Provider -provider "aws" { - region = var.aws_region -} - -# Configure required providers -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - } - - required_version = ">= 1.2.0" -} diff --git a/terraform/rds.tf b/terraform/rds.tf deleted file mode 100644 index 46888f3..0000000 --- a/terraform/rds.tf +++ /dev/null @@ -1,66 +0,0 @@ -# RDS PostgreSQL Instance - -# Security group for RDS -resource "aws_security_group" "rds" { - name = "orchestrator-rds-sg" - description = "Security group for RDS instance" - vpc_id = data.aws_vpc.default.id - - ingress { - from_port = 5432 - to_port = 5432 - protocol = "tcp" - security_groups = local.ecs_security_groups - description = "Allow PostgreSQL access from ECS tasks" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - description = "Allow all outbound traffic" - } - - tags = { - Name = "orchestrator-rds-sg" - } -} - -# RDS subnet group -resource "aws_db_subnet_group" "orchestrator" { - name = "orchestrator-subnet-group" - subnet_ids = local.subnet_ids - - tags = { - Name = "Orchestrator DB subnet group" - } -} - -# RDS PostgreSQL Instance -resource "aws_db_instance" "orchestrator" { - identifier = "orchestrator-db" - engine = "postgres" - engine_version = "13" - instance_class = "db.t3.micro" - allocated_storage = 20 - storage_type = "gp2" - - db_name = var.db_name - username = var.local_db_user - password = var.local_db_password - - vpc_security_group_ids = [aws_security_group.rds.id] - db_subnet_group_name = aws_db_subnet_group.orchestrator.name - - skip_final_snapshot = true - publicly_accessible = false - - backup_retention_period = 7 - backup_window = "03:00-04:00" - maintenance_window = "Mon:04:00-Mon:05:00" - - tags = { - Name = "orchestrator-db" - } -} diff --git a/terraform/secrets.tf b/terraform/secrets.tf deleted file mode 100644 index 7a89ef4..0000000 --- a/terraform/secrets.tf +++ /dev/null @@ -1,62 +0,0 @@ -# AWS Secrets Manager resources - -# Database credentials secret -resource "aws_secretsmanager_secret" "db_credentials" { - name = "${var.secrets_prefix}/${var.db_credentials_secret_name}" - description = "Database credentials for the orchestrator service" - - lifecycle { - ignore_changes = [name] - } -} - -resource "aws_secretsmanager_secret_version" "db_credentials" { - secret_id = aws_secretsmanager_secret.db_credentials.id - secret_string = jsonencode({ - username = var.local_db_user - password = var.local_db_password - }) -} - -# Wallet secret -resource "aws_secretsmanager_secret" "wallet" { - name = "${var.secrets_prefix}/${var.wallet_secret_name}" - description = "Wallet JSON for the orchestrator service" - - lifecycle { - ignore_changes = [name] - } -} - -resource "aws_secretsmanager_secret_version" "wallet" { - secret_id = aws_secretsmanager_secret.wallet.id - secret_string = var.local_wallet_json -} - - -# IAM policy for ECS tasks to access secrets -data "aws_iam_policy_document" "secrets_access" { - statement { - effect = "Allow" - actions = [ - "secretsmanager:GetSecretValue", - "secretsmanager:DescribeSecret" - ] - resources = [ - aws_secretsmanager_secret.db_credentials.arn, - aws_secretsmanager_secret.wallet.arn - ] - } -} - -resource "aws_iam_policy" "secrets_access" { - name = "orchestrator-secrets-access" - description = "Allow access to orchestrator secrets" - policy = data.aws_iam_policy_document.secrets_access.json -} - -# Attach secrets policy to ECS task role -resource "aws_iam_role_policy_attachment" "ecs_task_secrets" { - role = aws_iam_role.ecs_task_role.name - policy_arn = aws_iam_policy.secrets_access.arn -} diff --git a/terraform/security.tf b/terraform/security.tf deleted file mode 100644 index 7a0545d..0000000 --- a/terraform/security.tf +++ /dev/null @@ -1,33 +0,0 @@ -# Security Groups - -# Security group for ECS tasks -resource "aws_security_group" "ecs_tasks" { - name = "orchestrator-ecs-tasks-sg" - description = "Security group for ECS tasks" - vpc_id = data.aws_vpc.default.id - - ingress { - from_port = 0 - to_port = 0 - protocol = "-1" - self = true - description = "Allow all traffic between tasks" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - description = "Allow all outbound traffic" - } - - tags = { - Name = "orchestrator-ecs-tasks-sg" - } -} - -# Update references to use the new security group -locals { - ecs_security_groups = [aws_security_group.ecs_tasks.id] -} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example deleted file mode 100644 index fcfa02b..0000000 --- a/terraform/terraform.tfvars.example +++ /dev/null @@ -1,31 +0,0 @@ -# AWS Region -aws_region = "us-east-1" - -# Provider Configuration -provider_id = "your-provider-id" # Set this to your unique provider identifier - -# Database Configuration (for local development/testing) -local_db_user = "myuser" # Change this -local_db_password = "mypassword" # Change this -db_name = "orchestrator_db" - -# Wallet Configuration -# Copy the contents of your wallet.json file and paste it here -local_wallet_json = < Date: Mon, 14 Apr 2025 14:50:14 -0400 Subject: [PATCH 46/80] added new cu code --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 48 ++----- orchestrator/src/containerManagment.ts | 65 --------- orchestrator/src/helperFunctions.ts | 190 +++++++++++++++++-------- requester/package.json | 3 +- requester/src/app.ts | 22 +-- requester/src/extra.ts | 60 ++++++++ 9 files changed, 225 insertions(+), 169 deletions(-) create mode 100644 requester/src/extra.ts diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 4fff7ae..b54467e 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.4.55 + image: randao/orchestrator:v0.4.60 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index d7f3b1b..fcff170 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.4.55 # You can change this value to any version you want +export VERSION=v0.4.60 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 051e67b..11369e6 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^6.0.3", + "ao-process-clients": "^6.0.16", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index f6c9c44..794fef9 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,10 +1,9 @@ -import { Client } from 'pg'; import Docker from 'dockerode'; import AWS from 'aws-sdk'; -import { connectWithRetry, dbConfig, setupDatabase } from './db_tools.js'; +import { connectWithRetry, setupDatabase } from './db_tools.js'; import Arweave from 'arweave'; -import { cleanupFulfilledEntries, getProviderRequests, getRandomClient, processChallengeRequests, processOutputRequests } from './helperFunctions.js'; -import { checkAndFetchIfNeeded, monitorDockerContainers } from './containerManagment.js'; +import { checkAndFetchIfNeeded, cleanupFulfilledEntries, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown } from './helperFunctions.js'; +import {monitorDockerContainers } from './containerManagment.js'; @@ -16,7 +15,7 @@ export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; export const DOCKER_MONITORING_TIME= 30000; -export const POLLING_INTERVAL_MS = 30000; //30 seconds +export const POLLING_INTERVAL_MS = 500; //0.5 seconds export const DATABASE_CHECK_TIME = 60000; //60 seconds export const MINIMUM_ENTRIES = 1000; export const DRYRUNTIMEOUT = 30000; // 30 seconds @@ -50,20 +49,6 @@ function resetStepTracking() { }; } - -async function shutdown() { - try { - const randomClient = await getRandomClient(); - let message = await randomClient.updateProviderAvailableValues(0); - console.log(message); - console.log(`Updated provider values to 0`); - } catch (error) { - console.error("Failed to update provider values:", error); - } -} - - - function getLogId(): string { const randomId = Math.floor(10000 + Math.random() * 90000); // 5-digit random number const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false }); // HH:MM:SS format @@ -95,10 +80,6 @@ async function polling(client: any) { const s1 = Date.now(); console.log(`${logId} Step 1 started.`); const openRequests = await getProviderRequests(PROVIDER_ID, logId); - if(openRequests == false){ - console.log("Provider is set up and ready. Please stake to join network at https://providers_randao.ar.io") - return - } stepTracking.step1 = { completed: true, timeTaken: Date.now() - s1 }; console.log(`${logId} Step 1: Open requests fetched. Time taken: ${stepTracking.step1.timeTaken}ms`); @@ -123,6 +104,7 @@ async function polling(client: any) { console.log(`${logId} Step 4 started.`); //TODO enable this again later await cleanupFulfilledEntries(client, openRequests, logId); + await checkAndFetchIfNeeded(client) stepTracking.step4 = { completed: true, timeTaken: Date.now() - s4 }; console.log(`${logId} Step 4 completed. Time taken: ${stepTracking.step4.timeTaken}ms`); })(), @@ -150,16 +132,16 @@ async function run(): Promise { PROVIDER_ID = address }); - setInterval(async () => { - const res = await client.query('SELECT COUNT(*) as count FROM time_lock_puzzles'); - console.log(`Periodic log - Current database size: ${res.rows[0].count}`); - // Check and fetch entries for the database if needed - console.log("Step 0: Checking and fetching database entries if below threshold."); - checkAndFetchIfNeeded(client, PROVIDER_ID).catch((error) => { - console.error("Error in checkAndFetchIfNeeded:", error); - }); - - }, DATABASE_CHECK_TIME); + // setInterval(async () => { + // const res = await client.query('SELECT COUNT(*) as count FROM time_lock_puzzles'); + // console.log(`Periodic log - Current database size: ${res.rows[0].count}`); + // // Check and fetch entries for the database if needed + // console.log("Step 0: Checking and fetching database entries if below threshold."); + // checkAndFetchIfNeeded(client, PROVIDER_ID).catch((error) => { + // console.error("Error in checkAndFetchIfNeeded:", error); + // }); + + // }, DATABASE_CHECK_TIME); setInterval(async () => { await monitorDockerContainers(); diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts index 438a149..026a035 100644 --- a/orchestrator/src/containerManagment.ts +++ b/orchestrator/src/containerManagment.ts @@ -1,16 +1,11 @@ import { docker, DOCKER_NETWORK, ecs, ENVIRONMENT, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; import AWS from 'aws-sdk'; import { dbConfig } from "./db_tools"; -import { getProviderAvailableRandomValues, updateAvailableValuesAsync } from "./helperFunctions"; -import { Client } from "pg"; - export interface NetworkConfig { subnets: string[]; securityGroups: string[]; } - -let ongoingRequest = false; let spotInterruptions = 0; // Global variables to track polling status let pulledDockerimage = false; @@ -168,8 +163,6 @@ export async function getMoreRandom(currentCount: number) { const entriesNeeded = MINIMUM_ENTRIES - currentCount; console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); - ongoingRequest = true; - if (ongoingContainers.size > 0) { console.log("A puzzle-gen container is already running. Skipping new container launch."); return null; @@ -262,61 +255,3 @@ export async function monitorDockerContainers(): Promise { function isDockerError(error: unknown): error is { statusCode: number } { return typeof error === 'object' && error !== null && 'statusCode' in error && typeof (error as any).statusCode === 'number'; } - -// Function to check and fetch database entries as needed -export async function checkAndFetchIfNeeded(client: Client, PROVIDER_ID:string) { - try { - //Check if provider has been given a special signal - const on_chain_avalible_random = await getProviderAvailableRandomValues(PROVIDER_ID); - // Query current count of usable DB entries - const res = await client.query( - 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' - ); - const currentCount = parseInt(res.rows[0].count, 10); - console.log("Total usable DB entries: " + currentCount); - - switch (on_chain_avalible_random.availibleRandomValues) { - case -1: - console.log("Value is -1"); - console.log("Provider has been shut down by USER..."); - console.log("Go to the provider dashboard to turn back on"); - //TODO prepare for shutdown - //TODO the async causes it to ovewrite itself - break; - case -2: - console.log("Value is -2"); - console.log("Provider has been shut down by PROCESS..."); - console.log("This is due to One of the following: "); - console.log("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); - console.log("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)" ); - console.log("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); - console.log("Go to the provider dashboard to turn back on"); - break; - case -3: - console.log("Value is -3"); - console.log("Provider has been shut down by PROCESS..."); - console.log("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); - console.log("Go to the provider dashboard to turn back on"); - break; - default: - console.log("Value is not -1, -2, or -3"); - console.log("Provider is up and working"); - console.log("Onchain Value is "+ on_chain_avalible_random.availibleRandomValues) - console.log("Local Value is "+ currentCount) - if (Math.abs(on_chain_avalible_random.availibleRandomValues - currentCount) > UNCHAIN_VS_OFFCHAIN_MAX_DIF) { - console.log(`Updating available random values from ${on_chain_avalible_random.availibleRandomValues} to ${currentCount}`); - updateAvailableValuesAsync(currentCount); - } - } - if (ongoingRequest) return; // Prevent redundant operations - - // Check if more entries are needed - if (currentCount >= MINIMUM_ENTRIES) return; - getMoreRandom(currentCount) - - } catch (error) { - console.error('Error during check and fetch:', error); - } finally { - ongoingRequest = false; // Allow future operations - } -} \ No newline at end of file diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index b1f26c8..7ff242d 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -1,17 +1,19 @@ -import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, Logger, LogLevel, RandomClient } from "ao-process-clients"; +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, Logger, LogLevel, RandomClient, RequestList } from "ao-process-clients"; import { Client } from "pg"; -import { COMPLETION_RETENTION_PERIOD_MS, DRYRUNTIMEOUT } from "./app"; +import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; +import { getMoreRandom } from "./containerManagment"; let randomClientInstance: RandomClient | null = null; let lastInitTime: number = 0; const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds - -const AO_CONFIG = { - MU_URL: "https://ur-mu.randao.net", - CU_URL: "https://ur-cu.randao.net", - GATEWAY_URL: "https://arweave.net", -}; +let current_onchain_random = - 10 +let ongoingRequest = false; +// const AO_CONFIG = { +// MU_URL: "https://ur-mu.randao.net", +// CU_URL: "https://ur-cu.randao.net", +// GATEWAY_URL: "https://arweave.net", +// }; // Optional: Auto-reinitialize on a timer setInterval(() => { randomClientInstance = null; @@ -21,8 +23,8 @@ export async function getRandomClient(): Promise { const currentTime = Date.now(); Logger.setLogLevel(LogLevel.DEBUG) if (!randomClientInstance || (currentTime - lastInitTime) > REINIT_INTERVAL) { - randomClientInstance = ((await RandomClient.defaultBuilder()) - .withAOConfig(AO_CONFIG)) + randomClientInstance = ((await RandomClient.defaultBuilder())) + // .withAOConfig(AO_CONFIG) .withWallet(JSON.parse(process.env.WALLET_JSON!)) .build(); lastInitTime = currentTime; @@ -239,70 +241,131 @@ export async function cleanupFulfilledEntries( console.log(`${parentLogId} Step 4 completed.`); } +export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { + const defaultResponse: GetOpenRandomRequestsResponse = { + providerId: PROVIDER_ID, + activeChallengeRequests: { request_ids: [] }, + activeOutputRequests: { request_ids: [] } + }; + try { + const response = await (await getRandomClient()).getAllProviderActivity(); + const provider = response.find(p => p.provider_id === PROVIDER_ID); + if (!provider) { + console.warn(`${parentLogId} Warning: Provider with ID ${PROVIDER_ID} not found.`); + return defaultResponse; + } + // Attempt to parse fields if they exist, otherwise default to empty arrays + let parsedChallengeRequests: RequestList = { request_ids: [] }; + let parsedOutputRequests: RequestList = { request_ids: [] }; + try { + if (provider.active_challenge_requests) { + //@ts-ignore + parsedChallengeRequests = JSON.parse(provider.active_challenge_requests); + } + } catch (err) { + console.warn(`${parentLogId} Warning: Failed to parse active_challenge_requests:`, err); + } + try { + if (provider.active_output_requests) { + //@ts-ignore + parsedOutputRequests = JSON.parse(provider.active_output_requests); + } + } catch (err) { + console.warn(`${parentLogId} Warning: Failed to parse active_output_requests:`, err); + } + // Only update current_onchain_random if successful + current_onchain_random = provider.random_balance; + const result: GetOpenRandomRequestsResponse = { + providerId: provider.provider_id, + activeChallengeRequests: parsedChallengeRequests, + activeOutputRequests: parsedOutputRequests, + }; + console.log(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(result)}`); + console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${result.activeChallengeRequests.request_ids.length}`); + console.log(`${parentLogId} Step 1: Open Output Requests count: ${result.activeOutputRequests.request_ids.length}`); + return result; + } catch (error) { + console.error(`${parentLogId} Error fetching provider requests: ${error}`); + return defaultResponse; + } +} - - - - - - - -export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { - - let openRequests: GetOpenRandomRequestsResponse; - - // Create a function to fetch open requests with a timeout - const fetchOpenRequests = async (): Promise => { - try { - const response = await (await getRandomClient()).getOpenRandomRequests(PROVIDER_ID); - return response; - } catch (error) { - console.error(`${parentLogId} Error fetching requests: ${error}`); - return { /* Return a default or empty response here */ } as GetOpenRandomRequestsResponse; +// Function to check and fetch database entries as needed +export async function checkAndFetchIfNeeded(client: Client) { + try { + // Query current count of usable DB entries + const res = await client.query( + 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' + ); + const currentCount = parseInt(res.rows[0].count, 10); + console.log("Total usable DB entries: " + currentCount); + + switch (current_onchain_random) { + case -1: + console.log("Value is -1"); + console.log("Provider has been shut down by USER..."); + console.log("Go to the provider dashboard to turn back on"); + //TODO prepare for shutdown + //TODO the async causes it to ovewrite itself + break; + case -2: + console.log("Value is -2"); + console.log("Provider has been shut down by PROCESS..."); + console.log("This is due to One of the following: "); + console.log("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); + console.log("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)" ); + console.log("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); + console.log("Go to the provider dashboard to turn back on"); + break; + case -3: + console.log("Value is -3"); + console.log("Provider has been shut down by PROCESS..."); + console.log("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); + console.log("Go to the provider dashboard to turn back on"); + break; + case -10: + console.log("Value is -10"); + console.log("Provider has been turned on and is starting up OR is not staked yet"); + console.log("Go to the provider dashboard to Stke if you have not yet OR wait fro provider to finish turning on if you have staked already"); + break; + default: + console.log("Value is not -1, -2, or -3"); + console.log("Provider is up and working"); + console.log("Onchain Value is "+ current_onchain_random) + console.log("Local Value is "+ currentCount) + if (Math.abs(current_onchain_random - currentCount) > UNCHAIN_VS_OFFCHAIN_MAX_DIF) { + console.log(`Updating available random values from ${current_onchain_random} to ${currentCount}`); + updateAvailableValuesAsync(currentCount); + } } - }; + if (ongoingRequest) return; // Prevent redundant operations - try { - openRequests = await Promise.race([ - fetchOpenRequests().catch(err => { - throw new Error(`${parentLogId} Fetch Error: ${err}`); - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), DRYRUNTIMEOUT) - ) - ]); - } catch (error) { - console.log(`${parentLogId} Step 1: ${error}`); - //randclient.setDryRunAsMessage(true); - console.log("Removed this as its spamming") - console.log("Switching dryrun off"); - openRequests = await fetchOpenRequests(); // Retry request - } + // Check if more entries are needed + if (currentCount >= MINIMUM_ENTRIES) return; + getMoreRandom(currentCount) + ongoingRequest = true; - if(openRequests.toString().includes("not found")){ - return false + } catch (error) { + console.error('Error during check and fetch:', error); + } finally { + ongoingRequest = false; // Allow future operations } - console.log(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(openRequests)}`); - console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeChallengeRequests.request_ids.length}`); - console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${openRequests.activeOutputRequests.request_ids.length}`); - return openRequests; } export function updateAvailableValuesAsync(currentCount: number) { return (async () => { try { - const randomClient = await getRandomClient(); - await randomClient.updateProviderAvailableValues(currentCount); + await (await getRandomClient()).updateProviderAvailableValues(currentCount); console.log(`Updated provider values to ${currentCount}`); } catch (error) { console.error("Failed to update provider values:", error); @@ -310,9 +373,11 @@ export function updateAvailableValuesAsync(currentCount: number) { })(); } export async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise { - try { - const randomClient = await getRandomClient(); - return await randomClient.getProviderAvailableValues(PROVIDER_ID); + try { //TODO remove and clean this up + return { + providerId: PROVIDER_ID, + availibleRandomValues:current_onchain_random + } } catch (error) { console.error(`Error fetching available random values: ${error}`); return {} as GetProviderAvailableValuesResponse; @@ -393,4 +458,15 @@ const { } catch (error) { console.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); } +} + +export async function shutdown() { + try { + const randomClient = await getRandomClient(); + let message = await randomClient.updateProviderAvailableValues(0); + console.log(message); + console.log(`Updated provider values to 0`); + } catch (error) { + console.error("Failed to update provider values:", error); + } } \ No newline at end of file diff --git a/requester/package.json b/requester/package.json index 4d7aa9a..8b8705b 100644 --- a/requester/package.json +++ b/requester/package.json @@ -7,7 +7,8 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^5.4.16", + "@permaweb/aoconnect": "^0.0.78", + "ao-process-clients": "^6.0.16", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index 047de7f..fef9a8d 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -1,8 +1,9 @@ import { RandomClient, } from "ao-process-clients"; +import { TransferToProviders } from "./extra"; -const RETRY_DELAY_MS = 5000; // 5 seconds +const RETRY_DELAY_MS = 10000; // 10 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; @@ -10,20 +11,19 @@ const CHANCE_TO_CALL_RANDOM = 1; let cachedProviders: string[] = []; let lastProviderRefresh = 0; -const AO_CONFIG = { - MU_URL: "https://ur-mu.randao.net", - Cu_URL: "https://ur-cu.randao.net", - GATEWAY_URL: "https://arweave.net", -}; +// const AO_CONFIG = { +// MU_URL: "https://ur-mu.randao.net", +// CU_URL: "https://ur-cu.randao.net", +// GATEWAY_URL: "https://arweave.net", +// }; let randomClientInstance: RandomClient | null = null; async function getRandomClient(): Promise { if (!randomClientInstance) { - randomClientInstance = ((await RandomClient.defaultBuilder()) - .withAOConfig(AO_CONFIG)) - // .withProcessId("2ExUldxQ5NA_hnElSWYq0_lCBgeQQPxPhFbWDFihDEY") + randomClientInstance = ((await RandomClient.defaultBuilder())) + //.withAOConfig(AO_CONFIG) .withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)) .build(); } @@ -32,6 +32,7 @@ async function getRandomClient(): Promise { + let totalRandomCalled = 0; let totalTimeToFulfill = 0; let fulfilledRequests = 0; @@ -122,7 +123,8 @@ async function main() { const callbackId = `callback-${Date.now()}`; const { providers, count } = await getRandomProviders(randclient); console.log(`Selected ${count} providers:`, providers); - await randclient.createRequest(providers, count, callbackId); + //await randclient.createRequest(providers, count, callbackId); + await TransferToProviders(providers, callbackId) //await randclient.createRequest(["X1tqliRkKnClhVQ4aIeyuOaPTzr5PfnxqAoSdpTzZy8"], 1, "123"); totalRandomCalled++; console.log("Random request initiated. Awaiting request ID in open requests..."); diff --git a/requester/src/extra.ts b/requester/src/extra.ts new file mode 100644 index 0000000..7cb9233 --- /dev/null +++ b/requester/src/extra.ts @@ -0,0 +1,60 @@ +import { connect, createDataItemSigner } from "@permaweb/aoconnect"; + +const { spawn, message, result } = connect({ + MU_URL: "https://ur-mu.randao.net", + CU_URL: "https://ur-cu.randao.net", + MODE: "legacy" +}); +const TOKEN_PROCESS = "kUnKIPp7XS4pBsy1qy7j3S-JJ0ChZ4utbZ-IPxhw_W0" +const RAND_PROCESS = "UujLOtCfyo3uoKfuozS7cQfTe11BdvWR3Slb65zrR7k" +export async function fetchMessageResult( + messageID: string, + processID: string +): Promise<{ Messages: any[]; Spawns: any[]; Output: any[]; Error: any }> { + try { + const response = await result({ + message: messageID, + process: processID, + }); + + return { + Messages: response.Messages || [], + Spawns: response.Spawns || [], + Output: response.Output || [], + Error: response.Error || null + }; + } catch (error: any) { + if (error instanceof SyntaxError && error.message.includes("Unexpected token '<'")) { + console.error("CU timeout ratelimit error"); + await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 60 seconds + } else { + console.error("Error fetching message result:", error); + } + + return { Messages: [], Spawns: [], Output: [], Error: error }; + } +} +export async function TransferToProviders(providerIds: string[], callbackID:string) { + try { + const sentMessage = await message({ + process: TOKEN_PROCESS, + tags: [ + { name: "Action", value: "Transfer" }, + { name: "library", value: "npm install ao-process-clients" }, + { name: "Quantity", value: "100" }, + { name: "Recipient", value: RAND_PROCESS }, + { name: "X-CallbackId", value: callbackID }, + { name: "X-Providers", value: JSON.stringify({ provider_ids: providerIds }) }, + { name: "X-RequestedInputs", value: JSON.stringify({ requested_inputs: providerIds.length }) }, + ], + signer: createDataItemSigner(JSON.parse(process.env.REQUEST_WALLET_JSON!)), + data: "", + }); + + const result = await fetchMessageResult(sentMessage,TOKEN_PROCESS); + return result; + } catch (error) { + console.error("Transfer message failed:", error); + throw error; + } +} \ No newline at end of file From 9c914c0e76d2ecdf1da7d41295847dd12b6dc1c9 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 17 Apr 2025 11:32:23 -0400 Subject: [PATCH 47/80] added --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 2 +- orchestrator/src/helperFunctions.ts | 4 +++- requester/package.json | 2 +- requester/src/app.ts | 2 ++ requester/src/extra.ts | 14 +++++++++----- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index b54467e..8455f29 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.4.60 + image: randao/orchestrator:v0.4.65 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index fcff170..37c7765 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.4.60 # You can change this value to any version you want +export VERSION=v0.4.65 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 11369e6..fdbc234 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^6.0.16", + "ao-process-clients": "^6.0.18", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 794fef9..1a54a8f 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -15,7 +15,7 @@ export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; export const DOCKER_MONITORING_TIME= 30000; -export const POLLING_INTERVAL_MS = 500; //0.5 seconds +export const POLLING_INTERVAL_MS = 5000; //5 seconds export const DATABASE_CHECK_TIME = 60000; //60 seconds export const MINIMUM_ENTRIES = 1000; export const DRYRUNTIMEOUT = 30000; // 30 seconds diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 7ff242d..1ff00d1 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -12,6 +12,8 @@ let ongoingRequest = false; // const AO_CONFIG = { // MU_URL: "https://ur-mu.randao.net", // CU_URL: "https://ur-cu.randao.net", +// // MU_URL: "https://mu.ao-testnet.xyz", +// // CU_URL: "https://cu.ao-testnet.xyz", // GATEWAY_URL: "https://arweave.net", // }; // Optional: Auto-reinitialize on a timer @@ -24,7 +26,7 @@ export async function getRandomClient(): Promise { Logger.setLogLevel(LogLevel.DEBUG) if (!randomClientInstance || (currentTime - lastInitTime) > REINIT_INTERVAL) { randomClientInstance = ((await RandomClient.defaultBuilder())) - // .withAOConfig(AO_CONFIG) + //.withAOConfig(AO_CONFIG) .withWallet(JSON.parse(process.env.WALLET_JSON!)) .build(); lastInitTime = currentTime; diff --git a/requester/package.json b/requester/package.json index 8b8705b..1cda5f8 100644 --- a/requester/package.json +++ b/requester/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "@permaweb/aoconnect": "^0.0.78", - "ao-process-clients": "^6.0.16", + "ao-process-clients": "^6.0.18", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index fef9a8d..7ffd383 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -14,6 +14,8 @@ let lastProviderRefresh = 0; // const AO_CONFIG = { // MU_URL: "https://ur-mu.randao.net", // CU_URL: "https://ur-cu.randao.net", +// // MU_URL: "https://mu.ao-testnet.xyz", +// // CU_URL: "https://cu.ao-testnet.xyz", // GATEWAY_URL: "https://arweave.net", // }; diff --git a/requester/src/extra.ts b/requester/src/extra.ts index 7cb9233..16547fe 100644 --- a/requester/src/extra.ts +++ b/requester/src/extra.ts @@ -1,12 +1,16 @@ import { connect, createDataItemSigner } from "@permaweb/aoconnect"; const { spawn, message, result } = connect({ - MU_URL: "https://ur-mu.randao.net", - CU_URL: "https://ur-cu.randao.net", - MODE: "legacy" + MU_URL: "https://ur-mu.randao.net", + CU_URL: "https://ur-cu.randao.net", + // MU_URL: "https://mu.ao-testnet.xyz", + // CU_URL: "https://cu.ao-testnet.xyz", + GATEWAY_URL: "https://arweave.net", + MODE: "legacy" + }); -const TOKEN_PROCESS = "kUnKIPp7XS4pBsy1qy7j3S-JJ0ChZ4utbZ-IPxhw_W0" -const RAND_PROCESS = "UujLOtCfyo3uoKfuozS7cQfTe11BdvWR3Slb65zrR7k" +const TOKEN_PROCESS = "rPpsRk9Rm8_SJ1JF8m9_zjTalkv9Soaa_5U0tYUloeY" +const RAND_PROCESS = "ZBSQD_GeGUdQAiixxKy9Ag1rgJvJ_yFUGExwjW6mA7E" export async function fetchMessageResult( messageID: string, processID: string From d15ef70b83192204514a736369425378600dae05 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 18 Apr 2025 10:55:28 -0400 Subject: [PATCH 48/80] added raspberry pi puzzle gen --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 4 ++-- puzzle-generator/docs/developing.md | 23 ++++++++++++++++++----- requester/docker | 0 requester/src/app.ts | 2 +- 6 files changed, 23 insertions(+), 10 deletions(-) delete mode 100644 requester/docker diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 8455f29..7727bcc 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.4.65 + image: randao/orchestrator:v0.4.66 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 37c7765..f59f8aa 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.4.65 # You can change this value to any version you want +export VERSION=v0.4.66 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 1a54a8f..88b64d5 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -11,11 +11,11 @@ export const docker = new Docker(); export const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); export const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; -export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.1'; +export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.2'; export const DOCKER_MONITORING_TIME= 30000; -export const POLLING_INTERVAL_MS = 5000; //5 seconds +export const POLLING_INTERVAL_MS = 2500; //2.5 seconds export const DATABASE_CHECK_TIME = 60000; //60 seconds export const MINIMUM_ENTRIES = 1000; export const DRYRUNTIMEOUT = 30000; // 30 seconds diff --git a/puzzle-generator/docs/developing.md b/puzzle-generator/docs/developing.md index d15c15f..6a6291b 100644 --- a/puzzle-generator/docs/developing.md +++ b/puzzle-generator/docs/developing.md @@ -59,12 +59,25 @@ pytest --cov=src -# Build the Docker image with the version tag -docker build -t randao/puzzle-gen:latest -t randao/puzzle-gen:v0.1.1 . +# Set version as an environment variable +export VERSION=v0.1.5 # Change this value as needed -# Log in to Docker +# Initial build and tagging for local testing +docker build -t randao/puzzle-gen:latest -t randao/puzzle-gen:$VERSION . + +# Log in to Docker Hub (optional, remove if already logged in) docker login -# Push the image with the version tag +# Push local builds docker push randao/puzzle-gen:latest -docker push randao/puzzle-gen:v0.1.1 \ No newline at end of file +docker push randao/puzzle-gen:$VERSION + +# Set up and use Docker buildx builder (if not already created) +docker buildx create --name arm-builder --use || docker buildx use arm-builder +docker buildx inspect --bootstrap + +# Multi-platform build for ARM64 and AMD64, and push to Docker Hub +docker buildx build --platform linux/amd64,linux/arm64 \ + -t randao/puzzle-gen:latest \ + -t randao/puzzle-gen:$VERSION \ + --push . diff --git a/requester/docker b/requester/docker deleted file mode 100644 index e69de29..0000000 diff --git a/requester/src/app.ts b/requester/src/app.ts index 7ffd383..bff7611 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -3,7 +3,7 @@ import { } from "ao-process-clients"; import { TransferToProviders } from "./extra"; -const RETRY_DELAY_MS = 10000; // 10 seconds +const RETRY_DELAY_MS = 3000; // 3 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; From 695caf0d01d42364aaa7f66315096518fc7bf6c0 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 18 Apr 2025 11:42:32 -0400 Subject: [PATCH 49/80] added raspberry pi puzzle gen2 --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 7727bcc..4490714 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.4.66 + image: randao/orchestrator:v0.4.67 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index f59f8aa..11173a5 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.4.66 # You can change this value to any version you want +export VERSION=v0.4.67 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 88b64d5..1c5131d 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -11,7 +11,7 @@ export const docker = new Docker(); export const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); export const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; -export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.2'; +export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; export const DOCKER_MONITORING_TIME= 30000; From 0e9d11fdd50c55395127680fdf5e33b55dcd69a5 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 18 Apr 2025 12:00:00 -0400 Subject: [PATCH 50/80] added raspberry pi puzzle gen3 --- docker-compose/docker-compose.yml | 3 +- orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 1 - orchestrator/src/containerManagment.ts | 82 +++++++++++--------------- 4 files changed, 36 insertions(+), 52 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 4490714..a76a590 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.4.67 + image: randao/orchestrator:v0.4.68 depends_on: postgres: condition: service_healthy @@ -28,7 +28,6 @@ services: DB_USER: ${DB_USER:-myuser} DB_PASSWORD: ${DB_PASSWORD:-mypassword} DB_NAME: ${DB_NAME:-mydatabase} - ENVIRONMENT: local PATH_TO_WALLET: /app/wallet.json # Path inside the container WALLET_JSON: ${WALLET_JSON} DOCKER_NETWORK: backend # Passing the network name diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 11173a5..d4a3f0a 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -16,7 +16,7 @@ npx ts-node src/clear_outputs.ts # Export version as an environment variable -export VERSION=v0.4.67 # You can change this value to any version you want +export VERSION=v0.4.68 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 1c5131d..5a8806c 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -9,7 +9,6 @@ import {monitorDockerContainers } from './containerManagment.js'; export const docker = new Docker(); export const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); -export const ENVIRONMENT = process.env.ENVIRONMENT || 'local'; export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts index 026a035..a9ac742 100644 --- a/orchestrator/src/containerManagment.ts +++ b/orchestrator/src/containerManagment.ts @@ -1,4 +1,4 @@ -import { docker, DOCKER_NETWORK, ecs, ENVIRONMENT, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; +import { docker, DOCKER_NETWORK, ecs, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; import AWS from 'aws-sdk'; import { dbConfig } from "./db_tools"; export interface NetworkConfig { @@ -9,6 +9,7 @@ export interface NetworkConfig { let spotInterruptions = 0; // Global variables to track polling status let pulledDockerimage = false; +let pullingImagePromise: Promise | null = null; // Add at the top-level scope (module-global) // Cache for network configuration let cachedNetworkConfig: NetworkConfig | null = null; const ongoingContainers = new Set(); // Track container IDs of running Docker containers @@ -82,58 +83,49 @@ export async function launchVDFTask( export async function triggerTimePuzzleJobPod(randomCount: number): Promise { - if (ENVIRONMENT === 'cloud') { - try { - console.log("Cloud environment detected. Launching ECS task."); - - // Get or refresh network configuration - if (!cachedNetworkConfig) { - console.log("Fetching network configuration..."); - cachedNetworkConfig = await getNetworkConfig(ecs); - console.log("Network config:", cachedNetworkConfig); - } + const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; - const taskArn = await launchVDFTask(ecs, cachedNetworkConfig, randomCount); - if (taskArn) { - ongoingContainers.add(taskArn); - console.log(`ECS task started successfully: ${taskArn}`); - return taskArn; - } - return null; - } catch (error: any) { - if (error?.code === 'SpotCapacityNotAvailableException') { - spotInterruptions++; - console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } else { - console.error("Error launching ECS task:", error); - // Reset network config cache on error to force refresh on next attempt - cachedNetworkConfig = null; - } + if (ongoingContainers.size > 0) { + console.log("A puzzle-gen container is already running. Skipping new container launch."); return null; } - } else { - const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; - console.log(`Starting Docker container with name: ${containerName}`); - try { - if (!pulledDockerimage) { - console.log(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); - await new Promise((resolve, reject) => { - docker.pull(TIME_PUZZLE_JOB_IMAGE, (err: any, stream: NodeJS.ReadableStream) => { - if (err) { - return reject(err); + + // Ensure only one pull operation at a time + if (!pulledDockerimage) { + if (!pullingImagePromise) { + pullingImagePromise = new Promise((resolve, reject) => { + console.log(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); + docker.pull(TIME_PUZZLE_JOB_IMAGE, (err: Error | null, stream: NodeJS.ReadableStream | undefined) => { + if (err || !stream) { + pullingImagePromise = null; // reset on error + return reject(err || new Error("Docker stream undefined")); } - docker.modem.followProgress(stream, (doneErr) => { - if (doneErr) reject(doneErr); - else resolve(true); + docker.modem.followProgress(stream, (doneErr: Error | null) => { + if (doneErr) { + pullingImagePromise = null; // reset on error + reject(doneErr); + } else { + pulledDockerimage = true; // Mark as pulled after success + resolve(); + } }); }); }); - pulledDockerimage = true; } + try { + await pullingImagePromise; + } catch (error) { + console.error(`Failed to pull Docker image:`, error); + pullingImagePromise = null; + return null; + } + } + + console.log(`Starting Docker container with name: ${containerName}`); + try { const container = await docker.createContainer({ Image: TIME_PUZZLE_JOB_IMAGE, Cmd: ['sh', '-c', `python3 main.py ${randomCount}`], - Env: [ `DATABASE_TYPE=postgresql`, `DATABASE_HOST=${dbConfig.host}`, @@ -156,7 +148,6 @@ export async function triggerTimePuzzleJobPod(randomCount: number): Promise { // Function to wait for Docker containers to complete and remove them from tracking export async function monitorDockerContainers(): Promise { if (ongoingContainers.size === 0) return; - if (ENVIRONMENT === 'cloud') { - await monitorECSTasks(); - } else { - for (const containerId of ongoingContainers) { try { const container = docker.getContainer(containerId); @@ -248,7 +235,6 @@ export async function monitorDockerContainers(): Promise { ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) } } - } } // Helper function to type guard Docker errors From 3c1cfddc83b51a1e754a3c65fd69c9b8e2d36b6d Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 25 Apr 2025 09:52:10 -0400 Subject: [PATCH 51/80] working --- orchestrator/src/helperFunctions.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 1ff00d1..7ec1aaa 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -348,6 +348,8 @@ export async function checkAndFetchIfNeeded(client: Client) { if (Math.abs(current_onchain_random - currentCount) > UNCHAIN_VS_OFFCHAIN_MAX_DIF) { console.log(`Updating available random values from ${current_onchain_random} to ${currentCount}`); updateAvailableValuesAsync(currentCount); + }else{ + console.log(`Not updating onchain values to avoid uneeded onchain messages. When differeence is over ${UNCHAIN_VS_OFFCHAIN_MAX_DIF} an update will happen`) } } if (ongoingRequest) return; // Prevent redundant operations From ebe2450ad98ca1e4f2ff37a8e44fd49fb1466c93 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 25 Apr 2025 10:19:17 -0400 Subject: [PATCH 52/80] new logging --- docker-compose/.env.example | 1 + orchestrator/src/app.ts | 58 +++--- orchestrator/src/containerManagment.ts | 272 +++++++++++++------------ orchestrator/src/db_tools.ts | 19 +- orchestrator/src/helperFunctions.ts | 220 ++++++++++---------- orchestrator/src/logger.ts | 254 +++++++++++++++++++++++ orchestrator/src/reset_db.ts | 17 +- 7 files changed, 545 insertions(+), 296 deletions(-) create mode 100644 orchestrator/src/logger.ts diff --git a/docker-compose/.env.example b/docker-compose/.env.example index 62a84ab..e7c3864 100644 --- a/docker-compose/.env.example +++ b/docker-compose/.env.example @@ -2,6 +2,7 @@ DB_USER=myuser DB_PASSWORD=mypassword DB_NAME=mydatabase DOCKER_NETWORK=backend +LOG_CONSOLE_LEVEL=3 WALLET_JSON = '{ "kty": "RSA", "e": "test", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 5a8806c..6eceef7 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -3,16 +3,14 @@ import AWS from 'aws-sdk'; import { connectWithRetry, setupDatabase } from './db_tools.js'; import Arweave from 'arweave'; import { checkAndFetchIfNeeded, cleanupFulfilledEntries, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown } from './helperFunctions.js'; -import {monitorDockerContainers } from './containerManagment.js'; - - +import { monitorDockerContainers } from './containerManagment.js'; +import logger, { LogLevel, Logger } from './logger'; export const docker = new Docker(); export const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; - export const DOCKER_MONITORING_TIME= 30000; export const POLLING_INTERVAL_MS = 2500; //2.5 seconds export const DATABASE_CHECK_TIME = 60000; //60 seconds @@ -27,7 +25,6 @@ let PROVIDER_ID = ""; let pollingInProgress = false; let lastPollingId: string | null = null; - const arweave = Arweave.init({}); interface StepTracking { @@ -60,9 +57,9 @@ async function polling(client: any) { .filter(([_, data]) => data?.completed) .map(([step, data]) => `${step} (Time: ${data?.timeTaken}ms)`); - console.log(`\n[SKIPPED] Polling already in progress for ${lastPollingId}. Skipping this run.`); - console.log(`Completed steps so far: ${completedSteps.length > 0 ? completedSteps.join(", ") : "None"}`); - console.log("Current step tracking status:", stepTracking); // Debugging info to inspect tracking object + logger.debug(`\n[SKIPPED] Polling already in progress for ${lastPollingId}. Skipping this run.`); + logger.debug(`Completed steps so far: ${completedSteps.length > 0 ? completedSteps.join(", ") : "None"}`); + logger.verbose("Current step tracking status:", stepTracking); // Debugging info to inspect tracking object return; // Prevent concurrent execution } @@ -70,76 +67,77 @@ async function polling(client: any) { pollingInProgress = true; // Mark polling as in progress const logId = getLogId(); lastPollingId = logId; - console.log(`${logId} Starting Polling...`); + logger.info(`${logId} Starting Polling...`); try { const startTime = Date.now(); // Start time of polling // Step 1: Fetch open requests const s1 = Date.now(); - console.log(`${logId} Step 1 started.`); + logger.debug(`${logId} Step 1 started.`); const openRequests = await getProviderRequests(PROVIDER_ID, logId); stepTracking.step1 = { completed: true, timeTaken: Date.now() - s1 }; - console.log(`${logId} Step 1: Open requests fetched. Time taken: ${stepTracking.step1.timeTaken}ms`); + logger.debug(`${logId} Step 1: Open requests fetched. Time taken: ${stepTracking.step1.timeTaken}ms`); // Run Step 2, 3, and 4 concurrently await Promise.all([ (async () => { const s2 = Date.now(); - console.log(`${logId} Step 2 started.`); + logger.debug(`${logId} Step 2 started.`); await processChallengeRequests(client, openRequests.activeChallengeRequests, logId); stepTracking.step2 = { completed: true, timeTaken: Date.now() - s2 }; - console.log(`${logId} Step 2 completed. Time taken: ${stepTracking.step2.timeTaken}ms`); + logger.debug(`${logId} Step 2 completed. Time taken: ${stepTracking.step2.timeTaken}ms`); })(), (async () => { const s3 = Date.now(); - console.log(`${logId} Step 3 started.`); + logger.debug(`${logId} Step 3 started.`); await processOutputRequests(client, openRequests.activeOutputRequests, logId); stepTracking.step3 = { completed: true, timeTaken: Date.now() - s3 }; - console.log(`${logId} Step 3 completed. Time taken: ${stepTracking.step3.timeTaken}ms`); + logger.debug(`${logId} Step 3 completed. Time taken: ${stepTracking.step3.timeTaken}ms`); })(), (async () => { const s4 = Date.now(); - console.log(`${logId} Step 4 started.`); + logger.debug(`${logId} Step 4 started.`); //TODO enable this again later await cleanupFulfilledEntries(client, openRequests, logId); await checkAndFetchIfNeeded(client) stepTracking.step4 = { completed: true, timeTaken: Date.now() - s4 }; - console.log(`${logId} Step 4 completed. Time taken: ${stepTracking.step4.timeTaken}ms`); + logger.debug(`${logId} Step 4 completed. Time taken: ${stepTracking.step4.timeTaken}ms`); })(), ]); const totalTime = Date.now() - startTime; - console.log(`${logId} Polling cycle completed successfully. Total time taken: ${totalTime}ms`); + logger.info(`${logId} Polling cycle completed successfully. Total time taken: ${totalTime}ms`); } catch (error) { - console.error(`${logId} An error occurred during polling:`, error); + logger.error(`${logId} An error occurred during polling:`, error); } finally { pollingInProgress = false; // Reset flag after execution } } - - // Main function async function run(): Promise { + // Initialize logger from environment variables + logger.info("Orchestrator starting up"); + logger.debug("Environment variables loaded, initializing services"); + const client = await connectWithRetry(); await setupDatabase(client); arweave.wallets.jwkToAddress(JSON.parse(process.env.WALLET_JSON!)).then((address) => { - console.log(address); - PROVIDER_ID = address + logger.info(`Provider ID: ${address}`); + PROVIDER_ID = address; }); // setInterval(async () => { // const res = await client.query('SELECT COUNT(*) as count FROM time_lock_puzzles'); - // console.log(`Periodic log - Current database size: ${res.rows[0].count}`); + // logger.debug(`Periodic log - Current database size: ${res.rows[0].count}`); // // Check and fetch entries for the database if needed - // console.log("Step 0: Checking and fetching database entries if below threshold."); + // logger.debug("Step 0: Checking and fetching database entries if below threshold."); // checkAndFetchIfNeeded(client, PROVIDER_ID).catch((error) => { - // console.error("Error in checkAndFetchIfNeeded:", error); + // logger.error("Error in checkAndFetchIfNeeded:", error); // }); - // }, DATABASE_CHECK_TIME); setInterval(async () => { @@ -151,12 +149,12 @@ async function run(): Promise { }, POLLING_INTERVAL_MS); process.on("SIGTERM", async () => { - console.log("SIGTERM received. Closing database connection."); + logger.info("SIGTERM received. Shutting down gracefully."); await client.end(); await shutdown(); + await Logger.close(); // Use the static close method on the Logger class process.exit(0); }); } -run().catch((err) => console.error(`Error in main function: ${err}`)); - +run().catch((err) => logger.error(`Error in main function: ${err}`)); diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts index a9ac742..c5d7d09 100644 --- a/orchestrator/src/containerManagment.ts +++ b/orchestrator/src/containerManagment.ts @@ -1,6 +1,8 @@ import { docker, DOCKER_NETWORK, ecs, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; import AWS from 'aws-sdk'; import { dbConfig } from "./db_tools"; +import logger from "./logger"; + export interface NetworkConfig { subnets: string[]; securityGroups: string[]; @@ -79,165 +81,165 @@ export async function launchVDFTask( return taskArn || null; } - - - export async function triggerTimePuzzleJobPod(randomCount: number): Promise { - const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; - if (ongoingContainers.size > 0) { - console.log("A puzzle-gen container is already running. Skipping new container launch."); - return null; - } + if (ongoingContainers.size > 0) { + logger.debug("A puzzle-gen container is already running. Skipping new container launch."); + return null; + } - // Ensure only one pull operation at a time - if (!pulledDockerimage) { - if (!pullingImagePromise) { - pullingImagePromise = new Promise((resolve, reject) => { - console.log(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); - docker.pull(TIME_PUZZLE_JOB_IMAGE, (err: Error | null, stream: NodeJS.ReadableStream | undefined) => { - if (err || !stream) { - pullingImagePromise = null; // reset on error - return reject(err || new Error("Docker stream undefined")); - } - docker.modem.followProgress(stream, (doneErr: Error | null) => { - if (doneErr) { - pullingImagePromise = null; // reset on error - reject(doneErr); - } else { - pulledDockerimage = true; // Mark as pulled after success - resolve(); - } - }); - }); - }); - } - try { - await pullingImagePromise; - } catch (error) { - console.error(`Failed to pull Docker image:`, error); - pullingImagePromise = null; - return null; + // Ensure only one pull operation at a time + if (!pulledDockerimage) { + if (!pullingImagePromise) { + pullingImagePromise = new Promise((resolve, reject) => { + logger.info(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); + docker.pull(TIME_PUZZLE_JOB_IMAGE, (err: Error | null, stream: NodeJS.ReadableStream | undefined) => { + if (err || !stream) { + pullingImagePromise = null; // reset on error + return reject(err || new Error("Docker stream undefined")); + } + docker.modem.followProgress(stream, (doneErr: Error | null) => { + if (doneErr) { + pullingImagePromise = null; // reset on error + reject(doneErr); + } else { + pulledDockerimage = true; // Mark as pulled after success + resolve(); } - } + }); + }); + }); + } + try { + await pullingImagePromise; + } catch (error) { + logger.error(`Failed to pull Docker image:`, error); + pullingImagePromise = null; + return null; + } + } - console.log(`Starting Docker container with name: ${containerName}`); - try { - const container = await docker.createContainer({ - Image: TIME_PUZZLE_JOB_IMAGE, - Cmd: ['sh', '-c', `python3 main.py ${randomCount}`], - Env: [ - `DATABASE_TYPE=postgresql`, - `DATABASE_HOST=${dbConfig.host}`, - `DATABASE_PORT=${dbConfig.port.toString()}`, - `DATABASE_USER=${dbConfig.user}`, - `DATABASE_PASSWORD=${dbConfig.password}`, - `DATABASE_NAME=${dbConfig.database}`, - ], - HostConfig: { - NetworkMode: DOCKER_NETWORK, - }, - name: containerName - }); - - await container.start(); - ongoingContainers.add(container.id); - console.log(`Docker container ${containerName} started successfully.`); - return container.id; - } catch (error) { - console.error(`Error starting Docker container ${containerName}:`, error); - return null; - } + logger.info(`Starting Docker container with name: ${containerName}`); + try { + const container = await docker.createContainer({ + Image: TIME_PUZZLE_JOB_IMAGE, + Cmd: ['sh', '-c', `python3 main.py ${randomCount}`], + Env: [ + `DATABASE_TYPE=postgresql`, + `DATABASE_HOST=${dbConfig.host}`, + `DATABASE_PORT=${dbConfig.port.toString()}`, + `DATABASE_USER=${dbConfig.user}`, + `DATABASE_PASSWORD=${dbConfig.password}`, + `DATABASE_NAME=${dbConfig.database}`, + ], + HostConfig: { + NetworkMode: DOCKER_NETWORK, + }, + name: containerName + }); + + await container.start(); + ongoingContainers.add(container.id); + logger.info(`Docker container ${containerName} started successfully.`); + return container.id; + } catch (error) { + logger.error(`Error starting Docker container ${containerName}:`, error); + return null; + } } export async function getMoreRandom(currentCount: number) { - const entriesNeeded = MINIMUM_ENTRIES - currentCount; - console.log(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); + const entriesNeeded = MINIMUM_ENTRIES - currentCount; + logger.info(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${entriesNeeded} more entries...`); - if (ongoingContainers.size > 0) { - console.log("A puzzle-gen container is already running. Skipping new container launch."); - return null; - } + if (ongoingContainers.size > 0) { + logger.info("A puzzle-gen container is already running. Skipping new container launch."); + return null; + } - console.log(`Spawning a single container to generate ${entriesNeeded} random values.`); + logger.info(`Spawning a single container to generate ${entriesNeeded} random values.`); - try { - const jobId = await triggerTimePuzzleJobPod(entriesNeeded); - if (jobId) { - console.log(`Job triggered: ${jobId}`); - ongoingContainers.add(jobId); - } - } catch (error) { - console.error('Error triggering job pod:', error); + try { + const jobId = await triggerTimePuzzleJobPod(entriesNeeded); + if (jobId) { + logger.info(`Job triggered: ${jobId}`); + ongoingContainers.add(jobId); } + } catch (error) { + logger.error('Error triggering job pod:', error); + } } // Modified function to wait for ECS tasks to complete and remove them from tracking async function monitorECSTasks(): Promise { - if (ongoingContainers.size === 0) return; - - const describeTasksResult = await ecs.describeTasks({ - cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - tasks: Array.from(ongoingContainers) - }).promise(); - - describeTasksResult.tasks?.forEach(task => { - // Check capacity provider name to determine if it is running Fargate or Fargate Spot - const capacityProvider = task.capacityProviderName || 'Unknown'; - - console.log(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); - - if (task.lastStatus === 'STOPPED') { - console.log(`ECS task stopped: ${task.taskArn}`); - if (task.stoppedReason) { - console.log(`Task stopped reason: ${task.stoppedReason}`); - if (task.stoppedReason.includes('Host EC2 instance termination')) { - spotInterruptions++; - console.log(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } - } else { - console.log('Task stopped due to normal completion.'); - } - ongoingContainers.delete(task.taskArn as string); + if (ongoingContainers.size === 0) return; + + const describeTasksResult = await ecs.describeTasks({ + cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', + tasks: Array.from(ongoingContainers) + }).promise(); + + describeTasksResult.tasks?.forEach(task => { + // Check capacity provider name to determine if it is running Fargate or Fargate Spot + const capacityProvider = task.capacityProviderName || 'Unknown'; + + logger.debug(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); + + if (task.lastStatus === 'STOPPED') { + logger.info(`ECS task stopped: ${task.taskArn}`); + if (task.stoppedReason) { + logger.info(`Task stopped reason: ${task.stoppedReason}`); + if (task.stoppedReason.includes('Host EC2 instance termination')) { + spotInterruptions++; + logger.warn(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); } - }); + } else { + logger.info('Task stopped due to normal completion.'); + } + ongoingContainers.delete(task.taskArn as string); + } + }); } // Function to wait for Docker containers to complete and remove them from tracking export async function monitorDockerContainers(): Promise { - if (ongoingContainers.size === 0) return; - for (const containerId of ongoingContainers) { - try { - const container = docker.getContainer(containerId); - const containerInfo = await container.inspect(); - - // Check if the container is already stopped (exited) - if (containerInfo.State.Status === 'exited') { - console.log(`Docker container stopped: ${containerId}`); - - // Attempt to remove the container, handling possible errors gracefully - try { - await container.remove({ force: true }); // Force removal to avoid "in progress" errors - console.log(`Docker container removed: ${containerId}`); - ongoingContainers.delete(containerId); - } catch (removeError) { - if (isDockerError(removeError) && removeError.statusCode === 409) { - // Error 409 means removal is in progress, so skip this container for now - console.log(`Removal of container ${containerId} is already in progress. Skipping.`); - } else { - // Handle other errors that might occur during container removal - console.error(`Error removing Docker container ${containerId}:`, removeError); - } - } - } - } catch (error) { - console.error(`Error inspecting Docker container ${containerId}:`, error); - ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) - } + if (ongoingContainers.size === 0) return; + + logger.verbose(`Monitoring ${ongoingContainers.size} Docker containers`); + + for (const containerId of ongoingContainers) { + try { + const container = docker.getContainer(containerId); + const containerInfo = await container.inspect(); + + // Check if the container is already stopped (exited) + if (containerInfo.State.Status === 'exited') { + logger.debug(`Docker container stopped: ${containerId}`); + + // Attempt to remove the container, handling possible errors gracefully + try { + await container.remove({ force: true }); // Force removal to avoid "in progress" errors + logger.info(`Docker container removed: ${containerId}`); + ongoingContainers.delete(containerId); + } catch (removeError) { + if (isDockerError(removeError) && removeError.statusCode === 409) { + // Error 409 means removal is in progress, so skip this container for now + logger.debug(`Removal of container ${containerId} is already in progress. Skipping.`); + } else { + // Handle other errors that might occur during container removal + logger.error(`Error removing Docker container ${containerId}:`, removeError); + } } + } + } catch (error) { + logger.error(`Error inspecting Docker container ${containerId}:`, error); + ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) + } + } } // Helper function to type guard Docker errors function isDockerError(error: unknown): error is { statusCode: number } { - return typeof error === 'object' && error !== null && 'statusCode' in error && typeof (error as any).statusCode === 'number'; + return typeof error === 'object' && error !== null && 'statusCode' in error && typeof (error as any).statusCode === 'number'; } diff --git a/orchestrator/src/db_tools.ts b/orchestrator/src/db_tools.ts index ec93622..6b9f082 100644 --- a/orchestrator/src/db_tools.ts +++ b/orchestrator/src/db_tools.ts @@ -1,5 +1,6 @@ import { Client } from "pg"; import { MAX_RETRIES, RETRY_DELAY_MS } from "./app"; +import logger from "./logger"; export const dbConfig = { host: process.env.DB_HOST || 'localhost', @@ -11,19 +12,19 @@ export const dbConfig = { // Clear out the existing database export async function clearDatabase(client: Client): Promise { - console.log("Clearing database..."); + logger.info("Clearing database..."); await client.query(`SET session_replication_role = 'replica';`); const { rows } = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public';`); for (const row of rows) { - console.log(`Dropping table: ${row.tablename}`); + logger.debug(`Dropping table: ${row.tablename}`); await client.query(`DROP TABLE IF EXISTS "${row.tablename}" CASCADE;`); } await client.query(`SET session_replication_role = 'origin';`); - console.log("Database cleared."); + logger.info("Database cleared."); } export async function setupDatabase(client: Client): Promise { @@ -57,24 +58,26 @@ export async function setupDatabase(client: Client): Promise { // Drop the old verifiable_delay_functions table if exists await client.query(`DROP TABLE IF EXISTS verifiable_delay_functions CASCADE;`); - console.log("✅ Database setup complete or already exists."); + logger.info("✅ Database setup complete or already exists."); } catch (error: any) { - console.error("❌ Legitimate issue encountered during database setup:", error.message); + logger.error("❌ Legitimate issue encountered during database setup:", error.message); } } // Retry logic for connecting to PostgreSQL export async function connectWithRetry(): Promise { const client = new Client(dbConfig); + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { await client.connect(); - console.log(`Connected to PostgreSQL database (Attempt ${attempt})`); + logger.info(`Connected to PostgreSQL database (Attempt ${attempt})`); return client; } catch (error) { - console.error(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); + logger.warn(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); } } + throw new Error("Failed to connect to PostgreSQL after multiple attempts"); -} \ No newline at end of file +} diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 7ec1aaa..1a0adef 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -1,21 +1,15 @@ -import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, Logger, LogLevel, RandomClient, RequestList } from "ao-process-clients"; +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RequestList } from "ao-process-clients"; import { Client } from "pg"; import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; import { getMoreRandom } from "./containerManagment"; - +import logger, { LogLevel } from "./logger"; let randomClientInstance: RandomClient | null = null; let lastInitTime: number = 0; const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds let current_onchain_random = - 10 let ongoingRequest = false; -// const AO_CONFIG = { -// MU_URL: "https://ur-mu.randao.net", -// CU_URL: "https://ur-cu.randao.net", -// // MU_URL: "https://mu.ao-testnet.xyz", -// // CU_URL: "https://cu.ao-testnet.xyz", -// GATEWAY_URL: "https://arweave.net", -// }; + // Optional: Auto-reinitialize on a timer setInterval(() => { randomClientInstance = null; @@ -23,45 +17,39 @@ setInterval(() => { export async function getRandomClient(): Promise { const currentTime = Date.now(); - Logger.setLogLevel(LogLevel.DEBUG) + if (!randomClientInstance || (currentTime - lastInitTime) > REINIT_INTERVAL) { + logger.debug("Initializing RandomClient"); randomClientInstance = ((await RandomClient.defaultBuilder())) - //.withAOConfig(AO_CONFIG) .withWallet(JSON.parse(process.env.WALLET_JSON!)) .build(); lastInitTime = currentTime; + logger.debug("RandomClient initialized"); } return randomClientInstance; } - - - - - - - // Step 2: Process Challenge Requests (Database selection & assigning is atomic) export async function processChallengeRequests( client: Client, activeChallengeRequests: { request_ids: string[] } | undefined, parentLogId: string ): Promise { - console.log(`${parentLogId} Step 2: Processing challenge requests.`); + logger.info(`${parentLogId} Step 2: Processing challenge requests.`); if (!activeChallengeRequests || activeChallengeRequests.request_ids.length === 0) { - console.log(`${parentLogId} No Challenge Requests to process.`); + logger.info(`${parentLogId} No Challenge Requests to process.`); return; } const requestIds = activeChallengeRequests.request_ids; - console.log(`${parentLogId} Processing up to ${requestIds.length} requests.`); + logger.info(`${parentLogId} Processing up to ${requestIds.length} requests.`); try { await client.query('BEGIN'); // Start transaction - console.log(`${parentLogId} Fetching existing request mappings.`); + logger.debug(`${parentLogId} Fetching existing request mappings.`); // Fetch already assigned request_id -> dbId mappings const existingMappingsRes = await client.query( @@ -72,16 +60,16 @@ export async function processChallengeRequests( ); const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); - console.log(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); + logger.debug(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); // Find only the unmapped requests (requestIds not in existingRequestIds) const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); - console.log(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); + logger.debug(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); let mappedEntries: { requestId: string, dbId: number }[] = []; if (unmappedRequestIds.length > 0) { - console.log(`${parentLogId} Fetching available DB entries.`); + logger.debug(`${parentLogId} Fetching available DB entries.`); const dbRes = await client.query( `SELECT id FROM time_lock_puzzles WHERE request_id IS NULL @@ -92,7 +80,7 @@ export async function processChallengeRequests( ); const availableDbEntries = dbRes.rows.map(row => row.id); - console.log(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); + logger.debug(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); if (availableDbEntries.length > 0) { const numMappings = Math.min(unmappedRequestIds.length, availableDbEntries.length); @@ -105,10 +93,10 @@ export async function processChallengeRequests( [unmappedRequestIds[i], availableDbEntries[i]] ); mappedEntries.push({ requestId: unmappedRequestIds[i], dbId: availableDbEntries[i] }); - console.log(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); + logger.debug(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); } } else { - console.log(`${parentLogId} No available DB entries for unmapped requests.`); + logger.warn(`${parentLogId} No available DB entries for unmapped requests.`); } } @@ -116,62 +104,63 @@ export async function processChallengeRequests( const allRequestIds = [...existingRequestIds, ...mappedEntries.map(entry => entry.requestId)]; if (allRequestIds.length === 0) { - console.log(`${parentLogId} No requests to process. Committing transaction.`); + logger.info(`${parentLogId} No requests to process. Committing transaction.`); await client.query('COMMIT'); return; } await client.query('COMMIT'); // Commit all updates at once - console.log(`${parentLogId} Committed all changes. Now fulfilling challenges.`); + logger.info(`${parentLogId} Committed all changes. Now fulfilling challenges.`); // Call fulfillRandomChallenge for all request IDs await Promise.all( allRequestIds.map(requestId => fulfillRandomChallenge(client, requestId, parentLogId) - .catch(error => console.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) + .catch(error => logger.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) ) ); - console.log(`${parentLogId} All challenges fulfilled`); + logger.info(`${parentLogId} All challenges fulfilled`); } catch (error:any) { - console.error(`${parentLogId} Error in processChallengeRequests:`, error); + logger.error(`${parentLogId} Error in processChallengeRequests:`, error); await client.query('ROLLBACK'); // Rollback on failure - console.error(`SQL State: ${error.code}, Message: ${error.message}`); + logger.error(`SQL State: ${error.code}, Message: ${error.message}`); } } -// Step 3: Process Output Requests (unchanged but with logging) + +// Step 3: Process Output Requests export async function processOutputRequests( client: Client, activeOutputRequests: { request_ids: string[] } | undefined, parentLogId: string ): Promise { - console.log(`${parentLogId} Step 3: Processing output requests.`); + logger.info(`${parentLogId} Step 3: Processing output requests.`); if (!activeOutputRequests || activeOutputRequests.request_ids.length === 0) { - console.log(`${parentLogId} No Output Requests to process.`); + logger.info(`${parentLogId} No Output Requests to process.`); return; } const outputPromises = activeOutputRequests.request_ids.map(async (requestId) => { - console.log(`${parentLogId} Processing output request ID: ${requestId}`); + logger.debug(`${parentLogId} Processing output request ID: ${requestId}`); // Run fulfillRandomOutput asynchronously (do not await) fulfillRandomOutput(client, requestId, parentLogId) - .catch(error => console.error(`${parentLogId} Error fulfilling output:`, error)); + .catch(error => logger.error(`${parentLogId} Error fulfilling output:`, error)); }); await Promise.all(outputPromises); - console.log(`${parentLogId} Step 3 completed.`); + logger.info(`${parentLogId} Step 3 completed.`); } -// Step 4: Remove fulfilled entries no longer in use (unchanged but with logging) +// Step 4: Remove fulfilled entries no longer in use export async function cleanupFulfilledEntries( client: Client, openRequests: any, parentLogId: string ): Promise { - console.log(`${parentLogId} Step 4: Checking for fulfilled entries no longer in use.`); + logger.info(`${parentLogId} Step 4: Checking for fulfilled entries no longer in use.`); const now = new Date(); const cutoffTime = new Date(now.getTime() - COMPLETION_RETENTION_PERIOD_MS); @@ -214,10 +203,10 @@ export async function cleanupFulfilledEntries( SET detected_completed = NOW() WHERE id = ANY($1) `, [markAsCompleted]); - console.log(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); + logger.debug(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); } - // Delete old completed entries //TODO make sure its cleaning up BOTH tables + // Delete old completed entries if (markForDeletion.length > 0) { await client.query(` DELETE FROM rsa_keys @@ -231,16 +220,16 @@ export async function cleanupFulfilledEntries( WHERE id = ANY($1); `, [markForDeletion]); - console.log(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); + logger.info(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); } await client.query('COMMIT'); } catch (error) { - console.error(`${parentLogId} Error in cleanupFulfilledEntries:`, error); + logger.error(`${parentLogId} Error in cleanupFulfilledEntries:`, error); await client.query('ROLLBACK'); } - console.log(`${parentLogId} Step 4 completed.`); + logger.info(`${parentLogId} Step 4 completed.`); } export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { @@ -254,7 +243,7 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri const provider = response.find(p => p.provider_id === PROVIDER_ID); if (!provider) { - console.warn(`${parentLogId} Warning: Provider with ID ${PROVIDER_ID} not found.`); + logger.warn(`${parentLogId} Warning: Provider with ID ${PROVIDER_ID} not found.`); return defaultResponse; } @@ -268,7 +257,7 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri parsedChallengeRequests = JSON.parse(provider.active_challenge_requests); } } catch (err) { - console.warn(`${parentLogId} Warning: Failed to parse active_challenge_requests:`, err); + logger.warn(`${parentLogId} Warning: Failed to parse active_challenge_requests:`, err); } try { @@ -277,7 +266,7 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri parsedOutputRequests = JSON.parse(provider.active_output_requests); } } catch (err) { - console.warn(`${parentLogId} Warning: Failed to parse active_output_requests:`, err); + logger.warn(`${parentLogId} Warning: Failed to parse active_output_requests:`, err); } // Only update current_onchain_random if successful @@ -289,19 +278,18 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri activeOutputRequests: parsedOutputRequests, }; - console.log(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(result)}`); - console.log(`${parentLogId} Step 1: Open Challenge Requests count: ${result.activeChallengeRequests.request_ids.length}`); - console.log(`${parentLogId} Step 1: Open Output Requests count: ${result.activeOutputRequests.request_ids.length}`); + logger.verbose(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(result)}`); + logger.info(`${parentLogId} Step 1: Open Challenge Requests count: ${result.activeChallengeRequests.request_ids.length}`); + logger.info(`${parentLogId} Step 1: Open Output Requests count: ${result.activeOutputRequests.request_ids.length}`); return result; } catch (error) { - console.error(`${parentLogId} Error fetching provider requests: ${error}`); + logger.error(`${parentLogId} Error fetching provider requests: ${error}`); return defaultResponse; } } - // Function to check and fetch database entries as needed export async function checkAndFetchIfNeeded(client: Client) { try { @@ -310,57 +298,55 @@ export async function checkAndFetchIfNeeded(client: Client) { 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' ); const currentCount = parseInt(res.rows[0].count, 10); - console.log("Total usable DB entries: " + currentCount); + logger.info(`Total usable DB entries: ${currentCount}`); switch (current_onchain_random) { case -1: - console.log("Value is -1"); - console.log("Provider has been shut down by USER..."); - console.log("Go to the provider dashboard to turn back on"); - //TODO prepare for shutdown - //TODO the async causes it to ovewrite itself + logger.warn("Value is -1"); + logger.warn("Provider has been shut down by USER..."); + logger.warn("Go to the provider dashboard to turn back on"); break; case -2: - console.log("Value is -2"); - console.log("Provider has been shut down by PROCESS..."); - console.log("This is due to One of the following: "); - console.log("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); - console.log("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)" ); - console.log("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); - console.log("Go to the provider dashboard to turn back on"); + logger.error("Value is -2"); + logger.error("Provider has been shut down by PROCESS..."); + logger.error("This is due to One of the following: "); + logger.error("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); + logger.error("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)" ); + logger.error("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); + logger.error("Go to the provider dashboard to turn back on"); break; case -3: - console.log("Value is -3"); - console.log("Provider has been shut down by PROCESS..."); - console.log("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); - console.log("Go to the provider dashboard to turn back on"); + logger.warn("Value is -3"); + logger.warn("Provider has been shut down by PROCESS..."); + logger.warn("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); + logger.warn("Go to the provider dashboard to turn back on"); break; case -10: - console.log("Value is -10"); - console.log("Provider has been turned on and is starting up OR is not staked yet"); - console.log("Go to the provider dashboard to Stke if you have not yet OR wait fro provider to finish turning on if you have staked already"); + logger.info("Value is -10"); + logger.info("Provider has been turned on and is starting up OR is not staked yet"); + logger.info("Go to the provider dashboard to Stake if you have not yet OR wait for provider to finish turning on if you have staked already"); break; default: - console.log("Value is not -1, -2, or -3"); - console.log("Provider is up and working"); - console.log("Onchain Value is "+ current_onchain_random) - console.log("Local Value is "+ currentCount) + logger.debug("Provider is up and working"); + logger.debug(`Onchain Value is ${current_onchain_random}`); + logger.debug(`Local Value is ${currentCount}`); if (Math.abs(current_onchain_random - currentCount) > UNCHAIN_VS_OFFCHAIN_MAX_DIF) { - console.log(`Updating available random values from ${current_onchain_random} to ${currentCount}`); + logger.info(`Updating available random values from ${current_onchain_random} to ${currentCount}`); updateAvailableValuesAsync(currentCount); - }else{ - console.log(`Not updating onchain values to avoid uneeded onchain messages. When differeence is over ${UNCHAIN_VS_OFFCHAIN_MAX_DIF} an update will happen`) - } + } else { + logger.debug(`Not updating onchain values to avoid unneeded onchain messages. When difference is over ${UNCHAIN_VS_OFFCHAIN_MAX_DIF} an update will happen`); + } } if (ongoingRequest) return; // Prevent redundant operations // Check if more entries are needed if (currentCount >= MINIMUM_ENTRIES) return; - getMoreRandom(currentCount) + logger.info(`Less than ${MINIMUM_ENTRIES} entries found. Fetching more entries...`); + getMoreRandom(currentCount); ongoingRequest = true; } catch (error) { - console.error('Error during check and fetch:', error); + logger.error('Error during check and fetch:', error); } finally { ongoingRequest = false; // Allow future operations } @@ -370,20 +356,21 @@ export function updateAvailableValuesAsync(currentCount: number) { return (async () => { try { await (await getRandomClient()).updateProviderAvailableValues(currentCount); - console.log(`Updated provider values to ${currentCount}`); + logger.info(`Updated provider values to ${currentCount}`); } catch (error) { - console.error("Failed to update provider values:", error); + logger.error("Failed to update provider values:", error); } })(); } + export async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise { - try { //TODO remove and clean this up + try { return { providerId: PROVIDER_ID, - availibleRandomValues:current_onchain_random - } + availibleRandomValues: current_onchain_random + }; } catch (error) { - console.error(`Error fetching available random values: ${error}`); + logger.error(`Error fetching available random values:`, error); return {} as GetProviderAvailableValuesResponse; } } @@ -400,15 +387,15 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL ); if (!res.rowCount) { - console.error(`No entry found for Request ID: ${requestId}`); + logger.error(`No entry found for Request ID: ${requestId}`); return; } const { id: dbId, modulus, x: input } = res.rows[0]; - console.log(`${parentLogId} Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); + logger.debug(`${parentLogId} Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); - console.log(`${parentLogId} Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); + logger.info(`${parentLogId} Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); await (await getRandomClient()).commit({ requestId: requestId, puzzle: { @@ -416,11 +403,12 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL modulus: modulus } }); - console.log(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); + logger.info(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); } catch (error) { - console.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); + logger.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); } } + // Function to post VDF output and proof async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { try { @@ -438,39 +426,41 @@ async function fulfillRandomOutput(client: Client, requestId: string, parentLogI ); if (!res.rowCount) { - console.error(`No entry found for request ID: ${requestId}`); + logger.error(`No entry found for request ID: ${requestId}`); return; } - // Map the response to structured variables -const { - id: dbId, - output, // Mapping 'y' to 'output' - p: rsaP, - q: rsaQ -} = res.rows[0]; - console.log(`${parentLogId} Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}, output: ${output}, rsaP: ${rsaP}, rsaQ ${rsaQ} `); - - console.log(`${parentLogId} Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); + + // Map the response to structured variables + const { + id: dbId, + output, // Mapping 'y' to 'output' + p: rsaP, + q: rsaQ + } = res.rows[0]; + + logger.debug(`${parentLogId} Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}, output: ${output}, rsaP: ${rsaP}, rsaQ ${rsaQ} `); + + logger.info(`${parentLogId} Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); await (await getRandomClient()).reveal({ requestId: requestId, rsa_key: { p: rsaP, q: rsaQ } - }) - console.log(`${parentLogId} Proof posted for request ID: ${requestId}`); + }); + logger.info(`${parentLogId} Proof posted for request ID: ${requestId}`); } catch (error) { - console.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); + logger.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); } } export async function shutdown() { try { const randomClient = await getRandomClient(); - let message = await randomClient.updateProviderAvailableValues(0); - console.log(message); - console.log(`Updated provider values to 0`); + const message = await randomClient.updateProviderAvailableValues(0); + logger.info(String(message)); // Convert message to string + logger.info(`Updated provider values to 0`); } catch (error) { - console.error("Failed to update provider values:", error); + logger.error("Failed to update provider values:", error); } -} \ No newline at end of file +} diff --git a/orchestrator/src/logger.ts b/orchestrator/src/logger.ts new file mode 100644 index 0000000..8db7fdc --- /dev/null +++ b/orchestrator/src/logger.ts @@ -0,0 +1,254 @@ +import fs from 'fs'; +import path from 'path'; + +// Enum for different log levels +export enum LogLevel { + SILENT = 0, // No logging + ERROR = 1, // Only errors + WARN = 2, // Errors and warnings + INFO = 3, // Normal operational logs (default) + DEBUG = 4, // More detailed information + VERBOSE = 5 // Everything including detailed debugging +} + +// Log level names for better readability +const LogLevelNames: Record = { + [LogLevel.SILENT]: 'SILENT', + [LogLevel.ERROR]: 'ERROR', + [LogLevel.WARN]: 'WARN', + [LogLevel.INFO]: 'INFO', + [LogLevel.DEBUG]: 'DEBUG', + [LogLevel.VERBOSE]: 'VERBOSE' +}; + +export interface LoggerConfig { + consoleLogLevel: LogLevel; + fileLogLevel: LogLevel; + logFilePath: string; + maxLogFileSizeBytes: number; + rotateLogFiles: boolean; + maxLogFiles: number; +} + +export class Logger { + private static instance: Logger; + private config: LoggerConfig; + private logStream: fs.WriteStream | null = null; + + private constructor(config: LoggerConfig) { + this.config = config; + this.setupLogStream(); + this.logToFile(LogLevel.INFO, `Logger initialized with console level: ${LogLevelNames[config.consoleLogLevel]}, file level: ${LogLevelNames[config.fileLogLevel]}`); + } + + private setupLogStream(): void { + try { + // Create directory if it doesn't exist + const logDir = path.dirname(this.config.logFilePath); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + // Check if file exists and needs rotation + if (this.config.rotateLogFiles && fs.existsSync(this.config.logFilePath)) { + const stats = fs.statSync(this.config.logFilePath); + if (stats.size >= this.config.maxLogFileSizeBytes) { + this.rotateLogFiles(); + } + } + + // Create or open the log file + this.logStream = fs.createWriteStream(this.config.logFilePath, { flags: 'a' }); + + // Handle errors on the stream + this.logStream.on('error', (err) => { + console.error(`Error writing to log file: ${err}`); + }); + } catch (error) { + console.error(`Failed to setup log file: ${error}`); + } + } + + private rotateLogFiles(): void { + try { + for (let i = this.config.maxLogFiles - 1; i > 0; i--) { + const oldFile = `${this.config.logFilePath}.${i - 1}`; + const newFile = `${this.config.logFilePath}.${i}`; + + if (fs.existsSync(oldFile)) { + if (fs.existsSync(newFile)) { + fs.unlinkSync(newFile); + } + fs.renameSync(oldFile, newFile); + } + } + + const oldestFile = `${this.config.logFilePath}.0`; + if (fs.existsSync(this.config.logFilePath)) { + if (fs.existsSync(oldestFile)) { + fs.unlinkSync(oldestFile); + } + fs.renameSync(this.config.logFilePath, oldestFile); + } + } catch (error) { + console.error(`Failed to rotate log files: ${error}`); + } + } + + private formatLogEntry(level: LogLevel, message: string, ...args: any[]): string { + const timestamp = new Date().toISOString(); + const levelName = LogLevelNames[level]; + + // Format any objects in the args array + const formattedArgs = args.map(arg => { + if (typeof arg === 'object' && arg !== null) { + try { + return JSON.stringify(arg); + } catch (e) { + return String(arg); + } + } + return String(arg); + }); + + return `[${timestamp}] [${levelName}] ${message} ${formattedArgs.join(' ')}`.trim(); + } + + private logToConsole(level: LogLevel, message: string, ...args: any[]): void { + if (level <= this.config.consoleLogLevel) { + const formattedMessage = this.formatLogEntry(level, message, ...args); + + switch (level) { + case LogLevel.ERROR: + console.error(formattedMessage); + break; + case LogLevel.WARN: + console.warn(formattedMessage); + break; + default: + console.log(formattedMessage); + break; + } + } + } + + private logToFile(level: LogLevel, message: string, ...args: any[]): void { + if (this.logStream && level <= this.config.fileLogLevel) { + try { + const formattedMessage = this.formatLogEntry(level, message, ...args); + this.logStream.write(formattedMessage + '\n'); + } catch (error) { + console.error(`Failed to write to log file: ${error}`); + } + } + } + + public log(level: LogLevel, message: string, ...args: any[]): void { + this.logToConsole(level, message, ...args); + this.logToFile(level, message, ...args); + } + + public error(message: string, ...args: any[]): void { + this.log(LogLevel.ERROR, message, ...args); + } + + public warn(message: string, ...args: any[]): void { + this.log(LogLevel.WARN, message, ...args); + } + + public info(message: string, ...args: any[]): void { + this.log(LogLevel.INFO, message, ...args); + } + + public debug(message: string, ...args: any[]): void { + this.log(LogLevel.DEBUG, message, ...args); + } + + public verbose(message: string, ...args: any[]): void { + this.log(LogLevel.VERBOSE, message, ...args); + } + + // Static methods for singleton pattern + public static getInstance(): Logger { + if (!Logger.instance) { + Logger.initialize(); + } + return Logger.instance; + } + + public static initialize(config?: Partial): Logger { + // Default configuration + const defaultConfig: LoggerConfig = { + consoleLogLevel: this.parseLogLevel(process.env.LOG_CONSOLE_LEVEL) || LogLevel.INFO, + fileLogLevel: this.parseLogLevel(process.env.LOG_FILE_LEVEL) || LogLevel.VERBOSE, + logFilePath: process.env.LOG_FILE_PATH || path.join(process.cwd(), 'logs', 'orchestrator.log'), + maxLogFileSizeBytes: parseInt(process.env.LOG_MAX_SIZE || '10485760', 10), // 10MB default + rotateLogFiles: process.env.LOG_ROTATE === 'true', + maxLogFiles: parseInt(process.env.LOG_MAX_FILES || '5', 10) + }; + + // Merge with provided configuration + const mergedConfig = { ...defaultConfig, ...config }; + + if (Logger.instance) { + // Update configuration if instance already exists + Logger.instance.config = mergedConfig; + Logger.instance.logStream?.end(); + Logger.instance.setupLogStream(); + } else { + Logger.instance = new Logger(mergedConfig); + } + + return Logger.instance; + } + + // Utility to parse log level from string + private static parseLogLevel(level?: string): LogLevel | undefined { + if (!level) return undefined; + + // Try to parse numeric value + const numericLevel = parseInt(level, 10); + if (!isNaN(numericLevel) && numericLevel >= 0 && numericLevel <= 5) { + return numericLevel as LogLevel; + } + + // Parse string values + switch (level.toUpperCase()) { + case 'SILENT': return LogLevel.SILENT; + case 'ERROR': return LogLevel.ERROR; + case 'WARN': return LogLevel.WARN; + case 'INFO': return LogLevel.INFO; + case 'DEBUG': return LogLevel.DEBUG; + case 'VERBOSE': return LogLevel.VERBOSE; + default: return undefined; + } + } + + // Helper to update log level dynamically + public static setLogLevel(consoleLevel?: LogLevel, fileLevel?: LogLevel): void { + const instance = Logger.getInstance(); + if (consoleLevel !== undefined) { + instance.config.consoleLogLevel = consoleLevel; + } + if (fileLevel !== undefined) { + instance.config.fileLogLevel = fileLevel; + } + } + + // Close logger (for graceful shutdown) + public static close(): Promise { + return new Promise((resolve) => { + if (Logger.instance && Logger.instance.logStream) { + Logger.instance.logStream.end(() => { + Logger.instance.logStream = null; + resolve(); + }); + } else { + resolve(); + } + }); + } +} + +// Export default instance for convenience +export default Logger.getInstance(); diff --git a/orchestrator/src/reset_db.ts b/orchestrator/src/reset_db.ts index 8a58e79..3629f5e 100644 --- a/orchestrator/src/reset_db.ts +++ b/orchestrator/src/reset_db.ts @@ -1,5 +1,6 @@ import { Client } from 'pg'; import { dbConfig } from './db_tools'; +import logger from './logger'; interface TableRow { tablename: string; @@ -7,12 +8,12 @@ interface TableRow { // Function to connect to the database and drop all tables async function resetDatabase(): Promise { - console.log("Connecting to PostgreSQL to reset the database..."); + logger.info("Connecting to PostgreSQL to reset the database..."); const client = new Client(dbConfig); try { await client.connect(); - console.log("Connected to database. Dropping all tables..."); + logger.info("Connected to database. Dropping all tables..."); // Disable foreign key constraints (important for dropping tables safely) await client.query(`SET session_replication_role = 'replica';`); @@ -25,26 +26,26 @@ async function resetDatabase(): Promise { const tables = tablesRes.rows.map((row: TableRow) => row.tablename); if (tables.length === 0) { - console.log("No tables found in the database."); + logger.info("No tables found in the database."); } else { // Drop each table for (const table of tables) { - console.log(`Dropping table: ${table}`); + logger.info(`Dropping table: ${table}`); await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE;`); } - console.log("All tables dropped successfully."); + logger.info("All tables dropped successfully."); } // Re-enable foreign key constraints await client.query(`SET session_replication_role = 'origin';`); } catch (error) { - console.error("Error while resetting database:", error); + logger.error("Error while resetting database:", error); } finally { await client.end(); - console.log("Database connection closed."); + logger.info("Database connection closed."); } } // Run the reset function -resetDatabase().catch(console.error); +resetDatabase().catch(error => logger.error("Failed to reset database:", error)); From 259b0a9f620c7b64728086017020ccc69f5144e2 Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 6 May 2025 14:59:15 -0400 Subject: [PATCH 53/80] done --- orchestrator/docs/development.md | 3 +- orchestrator/puzzle-generator/.env.example | 7 + orchestrator/puzzle-generator/.gitignore | 8 ++ orchestrator/puzzle-generator/.pylintrc | 42 ++++++ orchestrator/puzzle-generator/Dockerfile | 35 +++++ orchestrator/puzzle-generator/README.md | 27 ++++ orchestrator/puzzle-generator/conftest.py | 5 + .../puzzle-generator/docs/developing.md | 83 ++++++++++++ orchestrator/puzzle-generator/main.py | 124 ++++++++++++++++++ .../puzzle-generator/requirements.txt | 7 + orchestrator/puzzle-generator/src/__init__.py | 0 .../src/converters/__init__.py | 6 + .../src/converters/rsa_converter.py | 25 ++++ .../converters/time_lock_puzzle_converter.py | 29 ++++ .../src/database/DatabaseService.py | 17 +++ .../puzzle-generator/src/database/__init__.py | 0 .../src/database/constants.py | 20 +++ .../puzzle-generator/src/database/database.py | 67 ++++++++++ .../src/database/entity/RSAEntity.py | 43 ++++++ .../database/entity/TimeLockPuzzleEntity.py | 49 +++++++ .../src/database/entity/__init__.py | 6 + .../src/database/initialize_db.py | 31 +++++ .../src/database/mixins/__init__.py | 0 .../src/database/mixins/saveable.py | 9 ++ orchestrator/puzzle-generator/src/mpc/MPC.py | 35 +++++ .../puzzle-generator/src/mpc/__init__.py | 7 + .../puzzle-generator/src/mpc/abstract/IMPC.py | 95 ++++++++++++++ .../src/mpc/abstract/__init__.py | 5 + .../puzzle-generator/src/mpc/types.py | 11 ++ .../puzzle-generator/src/primes/Primes.py | 18 +++ .../puzzle-generator/src/primes/__init__.py | 6 + .../src/primes/abstract/IPrimes.py | 18 +++ .../src/primes/abstract/__init__.py | 5 + .../src/protocol_constants.py | 7 + .../puzzle-generator/src/random/Random.py | 13 ++ .../puzzle-generator/src/random/__init__.py | 6 + .../src/random/abstract/IRandom.py | 18 +++ .../src/random/abstract/__init__.py | 5 + orchestrator/puzzle-generator/src/rsa/RSA.py | 52 ++++++++ .../puzzle-generator/src/rsa/__init__.py | 6 + .../puzzle-generator/src/rsa/abstract/IRSA.py | 46 +++++++ .../src/rsa/abstract/__init__.py | 0 .../EfficientTimeLockPuzzleSolver.py | 45 +++++++ .../SequentialTimeLockPuzzleSolver.py | 33 +++++ .../src/time_lock_puzzle/TimeLockPuzzle.py | 27 ++++ .../time_lock_puzzle/TimeLockPuzzleBuilder.py | 30 +++++ .../time_lock_puzzle/TimeLockPuzzleFactory.py | 78 +++++++++++ .../src/time_lock_puzzle/__init__.py | 25 ++++ .../IEfficientTimeLockPuzzleSolver.py | 36 +++++ .../ISequentialTimeLockPuzzleSolver.py | 19 +++ .../abstract/ITimeLockPuzzle.py | 30 +++++ .../abstract/ITimeLockPuzzleBuilder.py | 49 +++++++ .../abstract/ITimeLockPuzzleFactory.py | 34 +++++ .../src/time_lock_puzzle/abstract/__init__.py | 0 .../src/time_lock_puzzle/constants.py | 5 + requester/src/app.ts | 2 +- requester/src/extra.ts | 2 +- 57 files changed, 1408 insertions(+), 3 deletions(-) create mode 100644 orchestrator/puzzle-generator/.env.example create mode 100644 orchestrator/puzzle-generator/.gitignore create mode 100644 orchestrator/puzzle-generator/.pylintrc create mode 100644 orchestrator/puzzle-generator/Dockerfile create mode 100644 orchestrator/puzzle-generator/README.md create mode 100644 orchestrator/puzzle-generator/conftest.py create mode 100644 orchestrator/puzzle-generator/docs/developing.md create mode 100644 orchestrator/puzzle-generator/main.py create mode 100644 orchestrator/puzzle-generator/requirements.txt create mode 100644 orchestrator/puzzle-generator/src/__init__.py create mode 100644 orchestrator/puzzle-generator/src/converters/__init__.py create mode 100644 orchestrator/puzzle-generator/src/converters/rsa_converter.py create mode 100644 orchestrator/puzzle-generator/src/converters/time_lock_puzzle_converter.py create mode 100644 orchestrator/puzzle-generator/src/database/DatabaseService.py create mode 100644 orchestrator/puzzle-generator/src/database/__init__.py create mode 100644 orchestrator/puzzle-generator/src/database/constants.py create mode 100644 orchestrator/puzzle-generator/src/database/database.py create mode 100644 orchestrator/puzzle-generator/src/database/entity/RSAEntity.py create mode 100644 orchestrator/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py create mode 100644 orchestrator/puzzle-generator/src/database/entity/__init__.py create mode 100644 orchestrator/puzzle-generator/src/database/initialize_db.py create mode 100644 orchestrator/puzzle-generator/src/database/mixins/__init__.py create mode 100644 orchestrator/puzzle-generator/src/database/mixins/saveable.py create mode 100644 orchestrator/puzzle-generator/src/mpc/MPC.py create mode 100644 orchestrator/puzzle-generator/src/mpc/__init__.py create mode 100644 orchestrator/puzzle-generator/src/mpc/abstract/IMPC.py create mode 100644 orchestrator/puzzle-generator/src/mpc/abstract/__init__.py create mode 100644 orchestrator/puzzle-generator/src/mpc/types.py create mode 100644 orchestrator/puzzle-generator/src/primes/Primes.py create mode 100644 orchestrator/puzzle-generator/src/primes/__init__.py create mode 100644 orchestrator/puzzle-generator/src/primes/abstract/IPrimes.py create mode 100644 orchestrator/puzzle-generator/src/primes/abstract/__init__.py create mode 100644 orchestrator/puzzle-generator/src/protocol_constants.py create mode 100644 orchestrator/puzzle-generator/src/random/Random.py create mode 100644 orchestrator/puzzle-generator/src/random/__init__.py create mode 100644 orchestrator/puzzle-generator/src/random/abstract/IRandom.py create mode 100644 orchestrator/puzzle-generator/src/random/abstract/__init__.py create mode 100644 orchestrator/puzzle-generator/src/rsa/RSA.py create mode 100644 orchestrator/puzzle-generator/src/rsa/__init__.py create mode 100644 orchestrator/puzzle-generator/src/rsa/abstract/IRSA.py create mode 100644 orchestrator/puzzle-generator/src/rsa/abstract/__init__.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/__init__.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py create mode 100644 orchestrator/puzzle-generator/src/time_lock_puzzle/constants.py diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index d4a3f0a..6988f2a 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -10,7 +10,8 @@ npx ts-node src/clear_outputs.ts - +BUGS: +Random deletes itself from the db the moment its been used and not requested. It does not check if it has succeefully used it for challenge AND output first, just checks if its mapped it. This should not be an issue since it waits a day buttttt you know should be fixed with a better check diff --git a/orchestrator/puzzle-generator/.env.example b/orchestrator/puzzle-generator/.env.example new file mode 100644 index 0000000..ee29e47 --- /dev/null +++ b/orchestrator/puzzle-generator/.env.example @@ -0,0 +1,7 @@ +# Database configuration +DATABASE_TYPE=sqlite # Options: "sqlite" or "postgresql" +DATABASE_NAME=localdatabase.db # For SQLite, this will be the file path; for PostgreSQL, it's the database name +DATABASE_USER= # Required for PostgreSQL, leave blank for SQLite +DATABASE_PASSWORD= # Required for PostgreSQL, leave blank for SQLite +DATABASE_HOST=localhost # Required for PostgreSQL, typically "localhost" or an IP address +DATABASE_PORT=5432 # Default PostgreSQL port, leave as-is or set if using a different port diff --git a/orchestrator/puzzle-generator/.gitignore b/orchestrator/puzzle-generator/.gitignore new file mode 100644 index 0000000..628283d --- /dev/null +++ b/orchestrator/puzzle-generator/.gitignore @@ -0,0 +1,8 @@ +.env +venv/ +.pytest_cache/ +__pycache__/ +.vscode/ +.coverage +htmlcov/ +*.db \ No newline at end of file diff --git a/orchestrator/puzzle-generator/.pylintrc b/orchestrator/puzzle-generator/.pylintrc new file mode 100644 index 0000000..4b2ad19 --- /dev/null +++ b/orchestrator/puzzle-generator/.pylintrc @@ -0,0 +1,42 @@ +[MASTER] +# Add the gmpy2 module to the list of known third party modules +extension-pkg-whitelist=gmpy2 + +# Python code to execute, usually for sys.path manipulation such as pygtk.require() +init-hook='import sys; sys.path.append(".")' + +[MESSAGES CONTROL] +# Disable specific warnings +disable=C0111, # Missing docstring + C0103, # Invalid name + C0303, # Trailing whitespace + E1101, # No member (since gmpy2 uses dynamic members) + R0903, # Too Few public methods + +[TYPECHECK] +# List of module names for which member attributes should not be checked +ignored-modules=gmpy2 + +# List of classes names for which member attributes should not be checked +ignored-classes=gmpy2.mpz,gmpy2.random_state + +[FORMAT] +# Maximum number of characters on a single line +max-line-length=100 + +# Number of spaces of indent required inside a hanging or continued line +indent-after-paren=4 + +[BASIC] +# Regular expression which should only match function or class names +function-rgx=[a-z_][a-z0-9_]{2,50}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +[REPORTS] +# Set the output format. Available formats are text, parseable, colorized +output-format=colorized + +# Include a brief explanation of each error when errors are displayed +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} diff --git a/orchestrator/puzzle-generator/Dockerfile b/orchestrator/puzzle-generator/Dockerfile new file mode 100644 index 0000000..be0ffdf --- /dev/null +++ b/orchestrator/puzzle-generator/Dockerfile @@ -0,0 +1,35 @@ +# Use an official Python image as a base +FROM python:3.12 + +# Install system dependencies needed for gmpy2 and PostgreSQL connection +RUN apt-get update && apt-get install -y \ + libgmp-dev \ + libmpfr-dev \ + libmpc-dev \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Set up a working directory +WORKDIR /app + +# Copy only the requirements file to leverage Docker cache +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code +COPY . . + +# Set environment variables for PostgreSQL credentials (can be overridden at runtime) +ENV DB_NAME=mydatabase \ + DB_USER=myuser \ + DB_PASSWORD=mypassword \ + DB_HOST=localhost \ + DB_PORT=5432 + +# Expose any necessary ports (optional, specify if your app uses specific ports) +# EXPOSE 8000 + +# Command to run the main script +# CMD ["python", "main.py"] diff --git a/orchestrator/puzzle-generator/README.md b/orchestrator/puzzle-generator/README.md new file mode 100644 index 0000000..687e29f --- /dev/null +++ b/orchestrator/puzzle-generator/README.md @@ -0,0 +1,27 @@ +# [🔙](../) Time-Lock Puzzles +This repository section contains an implementation of [Time-Lock Puzzles](https://en.wikipedia.org/wiki/Time-lock_puzzle) as outlined in the seminal paper [Time-lock puzzles and timed-release Crypto](https://people.csail.mit.edu/rivest/pubs/RSW96.pdf) by Ronald L. Rivest, Adi Shamir, and David A. Wagner. + +This Time-Lock Puzzle implementation is part of **RandAO's Randomness Provider** project, designed to provide a reliable source of randomness based on cryptographic time delays. RandAO's Randomness Provider leverages Time-Lock Puzzles to ensure that randomness generation requires a precise amount of sequential computation time, establishing trust and security for applications requiring provably delayed randomness. + +## Table of Contents +- [Overview](#overview) +- [Development](#development) +- [License](#license) + +## Overview +The Time-Lock Puzzle implementation in this repository follows the specifications in the [RSW96 paper](https://people.csail.mit.edu/rivest/pubs/RSW96.pdf), providing a cryptographically secure mechanism for creating puzzles that require a predetermined amount of sequential computation to solve. This feature is crucial for applications in time-released cryptography and decentralized randomness protocols, where it is essential to produce randomness that cannot be accessed before a specific time has elapsed. + +Key features of this Time-Lock Puzzle implementation include: + + - Sequential Computation: The puzzle's design requires a specific number of sequential squaring operations modulo a composite number, ensuring that parallel computing offers no advantage in solving the puzzle. + - Precise Time Calibration: The difficulty of each puzzle can be precisely calibrated based on the computing power available to the solver. + - Efficient Creation: Puzzles can be created efficiently by anyone who knows the factorization of the modulus. + - Secure Message Encryption: The puzzle can securely encrypt a message that remains hidden until the sequential computation is completed. + +This approach enables decentralized protocols to produce randomness that is guaranteed to remain secret for a specific time period, making it ideal for use cases such as secure time-released cryptography, fair contract signing, sealed-bid auctions, and other applications requiring temporal security guarantees. + +## Development +For detailed development guidelines, including contributing, testing, and documentation, please refer to the [Development Documentation](./docs/developing.md). + +## License +This project is licensed under the MIT License. See the [LICENSE file](../LICENSE) for details. diff --git a/orchestrator/puzzle-generator/conftest.py b/orchestrator/puzzle-generator/conftest.py new file mode 100644 index 0000000..ace4b94 --- /dev/null +++ b/orchestrator/puzzle-generator/conftest.py @@ -0,0 +1,5 @@ +import sys +import os + +# Add the project root directory to sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) diff --git a/orchestrator/puzzle-generator/docs/developing.md b/orchestrator/puzzle-generator/docs/developing.md new file mode 100644 index 0000000..6a6291b --- /dev/null +++ b/orchestrator/puzzle-generator/docs/developing.md @@ -0,0 +1,83 @@ +# Project Setup +This guide will walk you through setting up and running the Time lock puzzle project in Python. + +## Prerequisites + - Python 3.7+: Make sure you have Python installed on your system. + - GMPY2: This library is required for high-performance modular arithmetic. It provides bindings to the GMP library for Python. + +## Setting Up a Virtual Environment +It’s recommended to use a virtual environment to manage dependencies for this project. +1. Create the Virtual Environment: +```bash +python3 -m venv venv +``` +2. Activate the Virtual Environment: + - On macOS and Linux: +```bash +source venv/bin/activate +``` + - On Windows: +```bash +.\venv\Scripts\activate +``` + +## Install Dependencies: +```bash +pip install -r requirements.txt +``` +Ensure that gmpy2 is installed. If you encounter issues, you may need to install GMP and MPFR on your system (e.g., sudo apt-get install libgmp-dev libmpfr-dev on Ubuntu). +```bash +sudo apt-get install libgmp-dev libmpfr-dev +``` + +## Initializing database +```bash +python src/database/initialize_db.py +``` + + +## Running the Project +To generate a VDF proof and verify it, run the main.py script: +```bash +python main.py 10 +``` +Required Command line Arguments: + - count: the number of time lock puzzles to generate and store in the database + +## Running the Tests +To run the unit tests, use the following command: +```bash +pytest +``` +With coverage: +```bash +pytest --cov=src +``` + + + + + + +# Set version as an environment variable +export VERSION=v0.1.5 # Change this value as needed + +# Initial build and tagging for local testing +docker build -t randao/puzzle-gen:latest -t randao/puzzle-gen:$VERSION . + +# Log in to Docker Hub (optional, remove if already logged in) +docker login + +# Push local builds +docker push randao/puzzle-gen:latest +docker push randao/puzzle-gen:$VERSION + +# Set up and use Docker buildx builder (if not already created) +docker buildx create --name arm-builder --use || docker buildx use arm-builder +docker buildx inspect --bootstrap + +# Multi-platform build for ARM64 and AMD64, and push to Docker Hub +docker buildx build --platform linux/amd64,linux/arm64 \ + -t randao/puzzle-gen:latest \ + -t randao/puzzle-gen:$VERSION \ + --push . diff --git a/orchestrator/puzzle-generator/main.py b/orchestrator/puzzle-generator/main.py new file mode 100644 index 0000000..a7d10d7 --- /dev/null +++ b/orchestrator/puzzle-generator/main.py @@ -0,0 +1,124 @@ +"""Main script for generating and persisting time lock puzzles.""" + +import argparse +import time +from typing import List, Tuple + +from src.converters.rsa_converter import RSAConverter +from src.converters.time_lock_puzzle_converter import TimeLockPuzzleConverter +from src.database.DatabaseService import DatabaseService +from src.database.entity.RSAEntity import RSAEntity +from src.database.entity.TimeLockPuzzleEntity import TimeLockPuzzleEntity +from src.mpc import MPC +from src.mpc.types import MPZ +from src.protocol_constants import BIT_SIZE, TIMING_PARAMETER +from src.rsa.RSA import RSA +from src.time_lock_puzzle.TimeLockPuzzle import TimeLockPuzzle +from src.time_lock_puzzle.TimeLockPuzzleFactory import TimeLockPuzzleFactory + + +class TimeLockPuzzleService: + """Service class for managing time lock puzzle operations.""" + + def __init__(self, bit_size: int, timing_parameter: MPC.mpz): + """ + Initialize the service. + + Args: + bit_size: Size for RSA parameters + timing_parameter: Number of squarings required + """ + self.factory = TimeLockPuzzleFactory(bit_size, timing_parameter) + self.rsa_converter = RSAConverter() + self.puzzle_converter = TimeLockPuzzleConverter() + + def generate_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + """ + Generate multiple time lock puzzles. + + Args: + amount: Number of puzzles to generate + + Returns: + List of (puzzle, rsa) tuples + """ + print(f"Generating {amount} puzzles...") + start_time = time.time() + puzzles = self.factory.create_puzzles(amount) + + total_time = time.time() - start_time + print(f"Puzzle generation took {total_time:.2f} seconds") + return puzzles + + def convert_to_entities( + self, puzzles: List[Tuple[TimeLockPuzzle, RSA, MPZ]] + ) -> List[TimeLockPuzzleEntity | RSAEntity]: + """ + Convert puzzles and RSAs to database entities. + + Args: + puzzles: List of (puzzle, rsa) tuples + + Returns: + List of entities to save + """ + print("\nConverting to entities...") + start_time = time.time() + entities = [] + for puzzle, rsa, y in puzzles: + # Convert RSA entity first to get its ID (now generated on creation) + rsa_entity = self.rsa_converter.to_entity(rsa) + # Create puzzle entity with the generated RSA ID + puzzle_entity = self.puzzle_converter.to_entity(puzzle, rsa_entity.id, y) + entities.extend([rsa_entity, puzzle_entity]) + total_time = time.time() - start_time + print(f"Entity conversion took {total_time:.2f} seconds") + return entities + + def save_entities(self, entities: List[TimeLockPuzzleEntity | RSAEntity]) -> None: + """ + Save entities to database. + + Args: + entities: List of entities to save + """ + print("\nSaving to database...") + start_time = time.time() + # Now we can save all entities at once since RSA IDs are generated on creation + DatabaseService.save_many(entities) + total_time = time.time() - start_time + print(f"Database save took {total_time:.2f} seconds") + + +def parse_args() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Generate and save time lock puzzles.") + parser.add_argument( + "count", + type=int, + help="Number of time lock puzzles to generate", + ) + return parser.parse_args() + + +def main() -> None: + """Generate time lock puzzles and save them to the database.""" + args = parse_args() + + # Initialize service + service = TimeLockPuzzleService(BIT_SIZE, TIMING_PARAMETER) + + # Generate puzzles + puzzles = service.generate_puzzles(args.count) + + # Convert to entities + entities = service.convert_to_entities(puzzles) + + # Save to database + service.save_entities(entities) + + print("\nDone!") + + +if __name__ == "__main__": + main() diff --git a/orchestrator/puzzle-generator/requirements.txt b/orchestrator/puzzle-generator/requirements.txt new file mode 100644 index 0000000..192aff3 --- /dev/null +++ b/orchestrator/puzzle-generator/requirements.txt @@ -0,0 +1,7 @@ +gmpy2 +sqlalchemy +psycopg2-binary +python-dotenv +pytest +pytest-cov +pytest-mock \ No newline at end of file diff --git a/orchestrator/puzzle-generator/src/__init__.py b/orchestrator/puzzle-generator/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orchestrator/puzzle-generator/src/converters/__init__.py b/orchestrator/puzzle-generator/src/converters/__init__.py new file mode 100644 index 0000000..ee1ed54 --- /dev/null +++ b/orchestrator/puzzle-generator/src/converters/__init__.py @@ -0,0 +1,6 @@ +"""Converters for database entities.""" + +from .time_lock_puzzle_converter import TimeLockPuzzleConverter +from .rsa_converter import RSAConverter + +__all__ = ["TimeLockPuzzleConverter", "RSAConverter"] diff --git a/orchestrator/puzzle-generator/src/converters/rsa_converter.py b/orchestrator/puzzle-generator/src/converters/rsa_converter.py new file mode 100644 index 0000000..209308c --- /dev/null +++ b/orchestrator/puzzle-generator/src/converters/rsa_converter.py @@ -0,0 +1,25 @@ +"""Converter for RSA objects.""" + +from src.rsa.RSA import RSA +from src.database.entity.RSAEntity import RSAEntity + + +class RSAConverter: + """Converter for storing RSA parameters in the database.""" + + @staticmethod + def to_entity(rsa: RSA) -> RSAEntity: + """Convert an RSA instance to an RSAEntity. + + Args: + rsa (RSA): The RSA instance to convert + + Returns: + RSAEntity: The database entity + """ + return RSAEntity( + hex(rsa.get_p())[2:], # remove 0x + hex(rsa.get_q())[2:], # remove 0x + hex(rsa.get_N())[2:], # remove 0x + hex(rsa.get_phi())[2:], # remove 0x + ) diff --git a/orchestrator/puzzle-generator/src/converters/time_lock_puzzle_converter.py b/orchestrator/puzzle-generator/src/converters/time_lock_puzzle_converter.py new file mode 100644 index 0000000..4eb498d --- /dev/null +++ b/orchestrator/puzzle-generator/src/converters/time_lock_puzzle_converter.py @@ -0,0 +1,29 @@ +"""Converter for time lock puzzle objects.""" + +from src.time_lock_puzzle.TimeLockPuzzle import TimeLockPuzzle +from src.database.entity.TimeLockPuzzleEntity import TimeLockPuzzleEntity +from src.mpc.types import MPZ + + +class TimeLockPuzzleConverter: + """Converter between TimeLockPuzzle and TimeLockPuzzleEntity.""" + + @staticmethod + def to_entity(puzzle: TimeLockPuzzle, rsa_id: str, y: MPZ) -> TimeLockPuzzleEntity: + """Convert a TimeLockPuzzle to a TimeLockPuzzleEntity. + + Args: + puzzle (TimeLockPuzzle): The puzzle to convert + rsa_id (str): ID of the associated RSA entity + y (MPZ): The y value from the puzzle tuple + + Returns: + TimeLockPuzzleEntity: The database entity + """ + return TimeLockPuzzleEntity( + x_hex=hex(puzzle.get_x())[2:], # remove 0x + y_hex=hex(y)[2:], # remove 0x + t=str(puzzle.get_t()), # remove 0x + N_hex=hex(puzzle.get_N())[2:], # remove 0x + rsa_id=rsa_id, + ) diff --git a/orchestrator/puzzle-generator/src/database/DatabaseService.py b/orchestrator/puzzle-generator/src/database/DatabaseService.py new file mode 100644 index 0000000..0df181e --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/DatabaseService.py @@ -0,0 +1,17 @@ +from typing import List +from .mixins.saveable import Saveable + + +class DatabaseService: + """Service class for database operations.""" + + @staticmethod + def save_many(instances: List[Saveable]) -> None: + """ + Save multiple instances to the database. + + Args: + instances: List of Saveable instances to save + """ + for instance in instances: + instance.save() diff --git a/orchestrator/puzzle-generator/src/database/__init__.py b/orchestrator/puzzle-generator/src/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orchestrator/puzzle-generator/src/database/constants.py b/orchestrator/puzzle-generator/src/database/constants.py new file mode 100644 index 0000000..f3822ae --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/constants.py @@ -0,0 +1,20 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + + +DATABASE_TYPE = os.getenv("DATABASE_TYPE", "sqlite") # sqlite or postgresql +DATABASE_NAME = os.getenv("DATABASE_NAME", "localdatabase.db") +DATABASE_USER = os.getenv("DATABASE_USER", "") +DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD", "") +DATABASE_HOST = os.getenv("DATABASE_HOST", "localhost") +DATABASE_PORT = os.getenv("DATABASE_PORT", "5432") # default port for PostgreSQL + +# Create the database URL based on the database type +if DATABASE_TYPE == "sqlite": + DATABASE_URL = f"sqlite:///{DATABASE_NAME}" +else: + DATABASE_URL = ( + f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" + ) diff --git a/orchestrator/puzzle-generator/src/database/database.py b/orchestrator/puzzle-generator/src/database/database.py new file mode 100644 index 0000000..f96fd27 --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/database.py @@ -0,0 +1,67 @@ +from sqlalchemy import create_engine, Engine +from sqlalchemy.orm import declarative_base, sessionmaker + +from src.database.constants import DATABASE_URL + +# Global variable to hold the singleton engine +_engine = None + + +def get_engine() -> Engine: + """ + Creates and returns a singleton SQLAlchemy engine connected to the database specified by DATABASE_URL. + + :return: SQLAlchemy Engine instance. + :rtype: sqlalchemy.engine.Engine + """ + global _engine + if _engine is None: + _engine = create_engine(DATABASE_URL) + return _engine + + +Base = declarative_base() # Single instance of Base + + +def get_orm_base(): + return Base + + +def save_instance(instance: any) -> None: + """ + Save an instance of an ORM model to the database. + + :param instance: The ORM model instance to save. + """ + engine = get_engine() + Session = sessionmaker(bind=engine) + session = Session() + + try: + session.add(instance) + session.commit() + except Exception as e: + session.rollback() + raise e + finally: + session.close() + + +def update_instance(instance: any) -> None: + """ + Update an instance of an ORM model in the database. + + :param instance: The ORM model instance to update. + """ + engine = get_engine() + Session = sessionmaker(bind=engine) + session = Session() + + try: + session.merge(instance) + session.commit() + except Exception as e: + session.rollback() + raise e + finally: + session.close() diff --git a/orchestrator/puzzle-generator/src/database/entity/RSAEntity.py b/orchestrator/puzzle-generator/src/database/entity/RSAEntity.py new file mode 100644 index 0000000..7ddb16f --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/entity/RSAEntity.py @@ -0,0 +1,43 @@ +import uuid +from sqlalchemy import Column, String +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship + +from src.database.mixins.saveable import Saveable +from src.database.database import get_orm_base + +# Define the Base class for ORM models +Base = get_orm_base() + + +class RSAEntity(Base, Saveable): + """Database entity for storing RSA parameters.""" + + __tablename__ = "rsa_keys" + + id = Column(String, primary_key=True) # Unique generated string ID + p = Column(String, nullable=False) # Store hex string of prime p + q = Column(String, nullable=False) # Store hex string of prime q + modulus = Column(String, nullable=False) # Store hex string of modulus N + phi = Column(String, nullable=False) # Store hex string of Euler's totient + puzzle = relationship( + "TimeLockPuzzleEntity", back_populates="rsa", uselist=False + ) # One-to-one back reference to puzzle + + def __repr__(self): + return f"" + + def __init__(self, p_hex: str, q_hex: str, N_hex: str, phi_hex: str): + """Initialize an RSA entity. + + Args: + p_hex (str): Hex string of prime p + q_hex (str): Hex string of prime q + N_hex (str): Hex string of modulus N + phi_hex (str): Hex string of Euler's totient + """ + self.id = str(uuid.uuid4()) # Generate ID on creation + self.p = p_hex + self.q = q_hex + self.modulus = N_hex + self.phi = phi_hex diff --git a/orchestrator/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py b/orchestrator/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py new file mode 100644 index 0000000..487f5db --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py @@ -0,0 +1,49 @@ +import uuid +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.orm import relationship + +from src.database.mixins.saveable import Saveable +from src.database.database import get_orm_base + +# Define the Base class for ORM models +Base = get_orm_base() + + +class TimeLockPuzzleEntity(Base, Saveable): + """Database entity for storing time lock puzzles.""" + + __tablename__ = "time_lock_puzzles" + + id = Column( + String, primary_key=True, default=lambda: str(uuid.uuid4()) + ) # Unique generated string ID + x = Column(String, nullable=False) # Store hex string of input value x + y = Column(String, nullable=False) # Store hex string of y value + t = Column(String, nullable=False) # Store base 10 string of time parameter t + modulus = Column(String, nullable=False) # Store hex string of modulus N + request_id = Column( + String, nullable=True + ) # Optional associated randomness request id (will be filled within the provider node runtime) + rsa_id = Column( + String, ForeignKey("rsa_keys.id"), nullable=False, unique=True + ) # One-to-one reference to RSA key + rsa = relationship( + "RSAEntity", back_populates="puzzle" + ) # One-to-one relationship to RSA entity + + def __repr__(self): + return f"" + + def __init__(self, x_hex: str, y_hex: str, t: str, N_hex: str, rsa_id: str): + """Initialize a time lock puzzle entity. + + Args: + x_hex (str): Hex string of input value x + t (str): Base 10 string of time parameter t + N_hex (str): Hex string of modulus N + """ + self.x = x_hex + self.y = y_hex + self.t = t + self.modulus = N_hex + self.rsa_id = rsa_id diff --git a/orchestrator/puzzle-generator/src/database/entity/__init__.py b/orchestrator/puzzle-generator/src/database/entity/__init__.py new file mode 100644 index 0000000..4f7ba70 --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/entity/__init__.py @@ -0,0 +1,6 @@ +"""Database entity models.""" + +from .TimeLockPuzzleEntity import TimeLockPuzzleEntity +from .RSAEntity import RSAEntity + +__all__ = ["TimeLockPuzzleEntity", "RSAEntity"] diff --git a/orchestrator/puzzle-generator/src/database/initialize_db.py b/orchestrator/puzzle-generator/src/database/initialize_db.py new file mode 100644 index 0000000..fb236c4 --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/initialize_db.py @@ -0,0 +1,31 @@ +# src/database/initialize_db.py + +import os +import sys +from sqlalchemy.exc import OperationalError + +# Dynamically add the `src` directory to `sys.path` +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) +from src.database.entity import * + +from src.database.database import get_engine, Base + + +def initialize_database(): + """ + Initializes the SQLite database by creating all tables defined in the ORM models. + """ + engine = get_engine() + try: + print("Initializing the database...") + Base.metadata.create_all(engine) + print("Database initialized successfully.") + except OperationalError as e: + print("Failed to initialize the database:", e) + finally: + engine.dispose() # Close the engine when done + + +if __name__ == "__main__": + + initialize_database() diff --git a/orchestrator/puzzle-generator/src/database/mixins/__init__.py b/orchestrator/puzzle-generator/src/database/mixins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orchestrator/puzzle-generator/src/database/mixins/saveable.py b/orchestrator/puzzle-generator/src/database/mixins/saveable.py new file mode 100644 index 0000000..b942ac6 --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/mixins/saveable.py @@ -0,0 +1,9 @@ +from src.database.database import save_instance + + +class Saveable: + def save(self) -> None: + """ + Save the instance to the database. + """ + save_instance(self) diff --git a/orchestrator/puzzle-generator/src/mpc/MPC.py b/orchestrator/puzzle-generator/src/mpc/MPC.py new file mode 100644 index 0000000..169c273 --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/MPC.py @@ -0,0 +1,35 @@ +import gmpy2 +from .abstract.IMPC import IMPC +from .types import MPZ, RandomState + + +class MPC(IMPC): + """Implementation of multi-precision computing operations.""" + + @staticmethod + def mpz(value: int) -> MPZ: + return gmpy2.mpz(value) + + @staticmethod + def random_state(seed: int) -> RandomState: + return gmpy2.random_state(seed) + + @staticmethod + def mpz_urandomb(state: RandomState, bit_count: int) -> MPZ: + return gmpy2.mpz_urandomb(state, bit_count) + + @staticmethod + def next_prime(value: MPZ) -> MPZ: + return gmpy2.next_prime(value) + + @staticmethod + def powmod(base: MPZ, exp: MPZ, mod: MPZ) -> MPZ: + return gmpy2.powmod(base, exp, mod) + + @staticmethod + def pow(base: MPZ, exp: MPZ) -> MPZ: + return base**exp + + @staticmethod + def mod(value: MPZ, modulus: MPZ) -> MPZ: + return value % modulus # gmpy2 supports % operator for mpz values diff --git a/orchestrator/puzzle-generator/src/mpc/__init__.py b/orchestrator/puzzle-generator/src/mpc/__init__.py new file mode 100644 index 0000000..ab216aa --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/__init__.py @@ -0,0 +1,7 @@ +"""Multi-precision computing module.""" + +from .MPC import MPC +from .abstract.IMPC import IMPC +from .types import MPZ, RandomState, T + +__all__ = ["MPC", "IMPC", "MPZ", "RandomState", "T"] diff --git a/orchestrator/puzzle-generator/src/mpc/abstract/IMPC.py b/orchestrator/puzzle-generator/src/mpc/abstract/IMPC.py new file mode 100644 index 0000000..4912662 --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/abstract/IMPC.py @@ -0,0 +1,95 @@ +from abc import ABC, abstractmethod +from ..types import MPZ, RandomState + + +class IMPC(ABC): + """Abstract base class defining the interface for multi-precision computing operations.""" + + @staticmethod + @abstractmethod + def mpz(value: int) -> MPZ: + """Convert a Python integer to an mpz. + + Args: + value (int): Integer value to convert + + Returns: + mpz: Multi-precision integer + """ + + @staticmethod + @abstractmethod + def random_state(seed: int) -> RandomState: + """Create a random state from a seed. + + Args: + seed (int): Seed value for random state + + Returns: + mpz: Random state object + """ + + @staticmethod + @abstractmethod + def mpz_urandomb(state: RandomState, bit_count: int) -> MPZ: + """Generate a random integer with specified number of bits. + + Args: + state (mpz): Random state to use + bit_count (int): Number of bits in result + + Returns: + mpz: Random integer + """ + + @staticmethod + @abstractmethod + def next_prime(value: MPZ) -> MPZ: + """Find the next prime number after the given value. + + Args: + value (mpz): Starting value + + Returns: + mpz: Next prime number + """ + + @staticmethod + @abstractmethod + def powmod(base: MPZ, exp: MPZ, mod: MPZ) -> MPZ: + """Compute (base ** exp) % mod efficiently. + + Args: + base (mpz): Base value + exp (mpz): Exponent value + mod (mpz): Modulus value + + Returns: + mpz: Result of modular exponentiation + """ + + @staticmethod + @abstractmethod + def pow(base: MPZ, exp: MPZ) -> MPZ: + """Compute base ** exp. + + Args: + base (mpz): Base value + exp (mpz): Exponent value + + Returns: + mpz: Result of exponentiation + """ + + @staticmethod + @abstractmethod + def mod(value: MPZ, modulus: MPZ) -> MPZ: + """Compute value % modulus. + + Args: + value (mpz): Value to reduce + modulus (mpz): Modulus to reduce by + + Returns: + mpz: Result of modular reduction + """ diff --git a/orchestrator/puzzle-generator/src/mpc/abstract/__init__.py b/orchestrator/puzzle-generator/src/mpc/abstract/__init__.py new file mode 100644 index 0000000..557827b --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for multi-precision computing operations.""" + +from .IMPC import IMPC + +__all__ = ["IMPC"] diff --git a/orchestrator/puzzle-generator/src/mpc/types.py b/orchestrator/puzzle-generator/src/mpc/types.py new file mode 100644 index 0000000..b9497fd --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/types.py @@ -0,0 +1,11 @@ +"""Type definitions for multi-precision computing operations.""" + +from typing import TypeVar, NewType +from gmpy2 import mpz as _mpz, random_state as _random_state + +# Define base types from gmpy2 +MPZ = NewType("MPZ", _mpz) +RandomState = NewType("RandomState", _random_state) + +# Generic type variable for numeric operations +T = TypeVar("T", MPZ, int) diff --git a/orchestrator/puzzle-generator/src/primes/Primes.py b/orchestrator/puzzle-generator/src/primes/Primes.py new file mode 100644 index 0000000..6615545 --- /dev/null +++ b/orchestrator/puzzle-generator/src/primes/Primes.py @@ -0,0 +1,18 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from ..random import Random +from .abstract.IPrimes import IPrimes + + +class Primes(IPrimes): + """Implementation of prime number generation.""" + + @staticmethod + def get_prime(bit_size: int) -> MPZ: + # Get random state for generating random numbers + rand = Random.get_random(bit_size) + + random_num = MPC.mpz_urandomb(rand, bit_size) + + # Get next prime after the random number + return MPC.next_prime(random_num) diff --git a/orchestrator/puzzle-generator/src/primes/__init__.py b/orchestrator/puzzle-generator/src/primes/__init__.py new file mode 100644 index 0000000..52cdf79 --- /dev/null +++ b/orchestrator/puzzle-generator/src/primes/__init__.py @@ -0,0 +1,6 @@ +"""Prime number generation module.""" + +from .Primes import Primes +from .abstract.IPrimes import IPrimes + +__all__ = ["Primes", "IPrimes"] diff --git a/orchestrator/puzzle-generator/src/primes/abstract/IPrimes.py b/orchestrator/puzzle-generator/src/primes/abstract/IPrimes.py new file mode 100644 index 0000000..2d49682 --- /dev/null +++ b/orchestrator/puzzle-generator/src/primes/abstract/IPrimes.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class IPrimes(ABC): + """Abstract base class defining the interface for prime number generation.""" + + @staticmethod + @abstractmethod + def get_prime(bit_size: int) -> MPZ: + """Get a random prime number. + + Args: + bit_size (int): Number of bits for the prime number. + + Returns: + MPZ: A random prime number + """ diff --git a/orchestrator/puzzle-generator/src/primes/abstract/__init__.py b/orchestrator/puzzle-generator/src/primes/abstract/__init__.py new file mode 100644 index 0000000..0ee899c --- /dev/null +++ b/orchestrator/puzzle-generator/src/primes/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for prime number generation.""" + +from .IPrimes import IPrimes + +__all__ = ["IPrimes"] diff --git a/orchestrator/puzzle-generator/src/protocol_constants.py b/orchestrator/puzzle-generator/src/protocol_constants.py new file mode 100644 index 0000000..15e0067 --- /dev/null +++ b/orchestrator/puzzle-generator/src/protocol_constants.py @@ -0,0 +1,7 @@ +# protocol_constants.py + +from src.mpc import MPC + + +BIT_SIZE = 2048 # RSA modulus bit size +TIMING_PARAMETER = MPC.mpz(3_000_000) # T - Total squarings for delay diff --git a/orchestrator/puzzle-generator/src/random/Random.py b/orchestrator/puzzle-generator/src/random/Random.py new file mode 100644 index 0000000..d5e1458 --- /dev/null +++ b/orchestrator/puzzle-generator/src/random/Random.py @@ -0,0 +1,13 @@ +import secrets +from ..mpc import MPC +from ..mpc.types import RandomState +from .abstract.IRandom import IRandom + + +class Random(IRandom): + """Implementation of secure random number generation.""" + + @staticmethod + def get_random(bit_size: int) -> RandomState: + secure_seed = secrets.randbits(bit_size) + return MPC.random_state(secure_seed) diff --git a/orchestrator/puzzle-generator/src/random/__init__.py b/orchestrator/puzzle-generator/src/random/__init__.py new file mode 100644 index 0000000..3c8b236 --- /dev/null +++ b/orchestrator/puzzle-generator/src/random/__init__.py @@ -0,0 +1,6 @@ +"""Random number generation module.""" + +from .Random import Random +from .abstract.IRandom import IRandom + +__all__ = ["Random", "IRandom"] diff --git a/orchestrator/puzzle-generator/src/random/abstract/IRandom.py b/orchestrator/puzzle-generator/src/random/abstract/IRandom.py new file mode 100644 index 0000000..2e6a0ce --- /dev/null +++ b/orchestrator/puzzle-generator/src/random/abstract/IRandom.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from ...mpc.types import RandomState + + +class IRandom(ABC): + """Abstract base class defining the interface for random number generation.""" + + @staticmethod + @abstractmethod + def get_random(bit_size: int) -> RandomState: + """Get a random state initialized with a secure seed. + + Args: + bit_size (int): Number of bits for the secure seed. + + Returns: + RandomState: A random state initialized with a secure seed + """ diff --git a/orchestrator/puzzle-generator/src/random/abstract/__init__.py b/orchestrator/puzzle-generator/src/random/abstract/__init__.py new file mode 100644 index 0000000..b22a9fd --- /dev/null +++ b/orchestrator/puzzle-generator/src/random/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for random number generation.""" + +from .IRandom import IRandom + +__all__ = ["IRandom"] diff --git a/orchestrator/puzzle-generator/src/rsa/RSA.py b/orchestrator/puzzle-generator/src/rsa/RSA.py new file mode 100644 index 0000000..45b5bae --- /dev/null +++ b/orchestrator/puzzle-generator/src/rsa/RSA.py @@ -0,0 +1,52 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from .abstract.IRSA import IRSA +from ..primes import Primes + + +class RSA(IRSA): + """Implementation of RSA cryptosystem.""" + + def __init__(self, bit_size: int) -> None: + """Initialize RSA by generating two random prime numbers. + + Args: + bit_size (int): Number of bits for RSA modulus. + Each prime will be bit_size/2 bits. + """ + # Generate two random prime numbers + prime_size = ( + bit_size // 2 - 1 + ) # Each prime is half the size TODO is this needed anymore with gmpc on chain? + self._p = Primes.get_prime(prime_size) + self._q = Primes.get_prime(prime_size) + + # Calculate modulus N and Euler's totient + self._N = self._calculate_N() + self._phi = self._calculate_phi() + + def get_p(self) -> MPZ: + return self._p + + def get_q(self) -> MPZ: + return self._q + + def get_N(self) -> MPZ: + return self._N + + def get_phi(self) -> MPZ: + return self._phi + + def get_eulers_totient(self) -> MPZ: + return self.get_phi() + + # Private methods + # -------------- + + def _calculate_N(self) -> MPZ: + """Calculate the RSA modulus N = p * q.""" + return MPC.mpz(self._p * self._q) + + def _calculate_phi(self) -> MPZ: + """Calculate Euler's totient φ(N) = (p-1)(q-1).""" + return MPC.mpz((self._p - 1) * (self._q - 1)) diff --git a/orchestrator/puzzle-generator/src/rsa/__init__.py b/orchestrator/puzzle-generator/src/rsa/__init__.py new file mode 100644 index 0000000..4ac513e --- /dev/null +++ b/orchestrator/puzzle-generator/src/rsa/__init__.py @@ -0,0 +1,6 @@ +"""RSA cryptosystem module.""" + +from .RSA import RSA +from .abstract.IRSA import IRSA + +__all__ = ["RSA", "IRSA"] diff --git a/orchestrator/puzzle-generator/src/rsa/abstract/IRSA.py b/orchestrator/puzzle-generator/src/rsa/abstract/IRSA.py new file mode 100644 index 0000000..9897017 --- /dev/null +++ b/orchestrator/puzzle-generator/src/rsa/abstract/IRSA.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class IRSA(ABC): + """Abstract base class defining the interface for RSA cryptosystem implementation.""" + + @abstractmethod + def get_p(self) -> MPZ: + """Get the first prime factor p. + + Returns: + MPZ: The prime number p + """ + + @abstractmethod + def get_q(self) -> MPZ: + """Get the second prime factor q. + + Returns: + MPZ: The prime number q + """ + + @abstractmethod + def get_N(self) -> MPZ: + """Get the modulus N = p * q. + + Returns: + MPZ: The modulus N + """ + + @abstractmethod + def get_phi(self) -> MPZ: + """Get Euler's totient φ(N) = (p-1)(q-1). + + Returns: + MPZ: The value of Euler's totient function + """ + + @abstractmethod + def get_eulers_totient(self) -> MPZ: + """Alias for get_phi(). + + Returns: + MPZ: The value of Euler's totient function + """ diff --git a/orchestrator/puzzle-generator/src/rsa/abstract/__init__.py b/orchestrator/puzzle-generator/src/rsa/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py new file mode 100644 index 0000000..996909f --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py @@ -0,0 +1,45 @@ +from multiprocessing import Pool +from typing import List, Tuple + +from ..mpc import MPC +from ..mpc.types import MPZ +from ..rsa.RSA import RSA +from .abstract.IEfficientTimeLockPuzzleSolver import IEfficientTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .constants import TWO + + +class EfficientTimeLockPuzzleSolver(IEfficientTimeLockPuzzleSolver): + """Implementation of efficient time lock puzzle solver using RSA private parameters.""" + + @staticmethod + def solve(rsa: RSA, puzzle: ITimeLockPuzzle) -> MPZ: + # Calculate y = x^(2^t) mod N efficiently using phi + # Calculate 2^t + exp = MPC.pow(TWO, puzzle.get_t()) # 2^t + phi = rsa.get_phi() + d = MPC.mod(exp, phi) # Reduce exponent modulo phi + return MPC.powmod(puzzle.get_x(), d, puzzle.get_N()) + + @staticmethod + def solve_many(puzzles: List[Tuple[RSA, ITimeLockPuzzle]]) -> List[MPZ]: + """ + Solve multiple time lock puzzles in parallel using multiprocessing. + + Args: + puzzles: List of tuples containing (RSA, puzzle) pairs to solve + + Returns: + List of solutions in the same order as input puzzles + """ + with Pool() as pool: + return pool.map(EfficientTimeLockPuzzleSolver._solve_single, puzzles) + + # Private Methods + # -------------- + + @staticmethod + def _solve_single(args: Tuple[RSA, ITimeLockPuzzle]) -> MPZ: + """Helper method to solve a single puzzle for multiprocessing.""" + rsa, puzzle = args + return EfficientTimeLockPuzzleSolver.solve(rsa, puzzle) diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py new file mode 100644 index 0000000..0144200 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py @@ -0,0 +1,33 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from .abstract.ISequentialTimeLockPuzzleSolver import ISequentialTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .constants import TWO + + +class SequentialTimeLockPuzzleSolver(ISequentialTimeLockPuzzleSolver): + """Implementation of sequential time lock puzzle solver.""" + + @staticmethod + def solve(puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle sequentially without RSA private parameters. + + This implementation: + 1. Calculates 2^t directly + 2. Then computes x^(2^t) mod N in one step using powmod + + Args: + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ + x = puzzle.get_x() + N = puzzle.get_N() + t = puzzle.get_t() + + # Calculate 2^t first + exp = MPC.pow(TWO, t) + + # Then calculate x^(2^t) mod N in one step + return MPC.powmod(x, exp, N) diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py new file mode 100644 index 0000000..65a0a51 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py @@ -0,0 +1,27 @@ +from ..mpc.types import MPZ +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle + + +class TimeLockPuzzle(ITimeLockPuzzle): + """Implementation of a time lock puzzle.""" + + def __init__(self, x: MPZ, t: MPZ, N: MPZ) -> None: + """Initialize a time lock puzzle. + + Args: + x (MPZ): The input value + t (MPZ): The time parameter + N (MPZ): The modulus + """ + self._x = x + self._t = t + self._N = N + + def get_x(self) -> MPZ: + return self._x + + def get_t(self) -> MPZ: + return self._t + + def get_N(self) -> MPZ: + return self._N diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py new file mode 100644 index 0000000..be6e9da --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py @@ -0,0 +1,30 @@ +from typing import Self +from ..mpc.types import MPZ +from .TimeLockPuzzle import TimeLockPuzzle +from .abstract.ITimeLockPuzzleBuilder import ITimeLockPuzzleBuilder + + +class TimeLockPuzzleBuilder(ITimeLockPuzzleBuilder): + """Implementation of time lock puzzle builder.""" + + def __init__(self) -> None: + self._x = None + self._t = None + self._N = None + + def set_x(self, x: MPZ) -> Self: + self._x = x + return self + + def set_t(self, t: MPZ) -> Self: + self._t = t + return self + + def set_N(self, N: MPZ) -> Self: + self._N = N + return self + + def build(self) -> TimeLockPuzzle: + if self._x is None or self._t is None or self._N is None: + raise ValueError("All parameters (x, t, N) must be set before building") + return TimeLockPuzzle(self._x, self._t, self._N) diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py new file mode 100644 index 0000000..19380a2 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py @@ -0,0 +1,78 @@ +from typing import List, Tuple +import multiprocessing + +from src.time_lock_puzzle import TimeLockPuzzleBuilder +from ..mpc import MPC +from ..mpc.types import MPZ +from ..random import Random +from ..rsa.RSA import RSA +from .TimeLockPuzzle import TimeLockPuzzle +from .EfficientTimeLockPuzzleSolver import EfficientTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzleFactory import ITimeLockPuzzleFactory + + +class TimeLockPuzzleFactory(ITimeLockPuzzleFactory): + """Implementation of time lock puzzle factory.""" + + def __init__(self, bit_size: int, timing_parameter: MPZ) -> None: + """Initialize the factory. + + Args: + bit_size (int): Number of bits for RSA parameters + timing_parameter (MPZ): Time parameter t for puzzles + """ + self._bit_size = bit_size + self._t = timing_parameter + + def create_puzzle(self) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + # Create RSA instance + rsa_instance = RSA(self._bit_size) + + # Generate random x + rand = Random.get_random(self._bit_size) + x = MPC.mpz_urandomb(rand, self._bit_size) + + # Create puzzle using builder + puzzle = ( + TimeLockPuzzleBuilder() + .set_x(x) + .set_t(self._t) + .set_N(rsa_instance.get_N()) + .build() + ) + + # Get solution using efficient solver + y = EfficientTimeLockPuzzleSolver.solve(rsa_instance, puzzle) + + return puzzle, rsa_instance, y + + def create_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + # Create parameters for each puzzle + puzzle_params = [(self._bit_size, self._t) for _ in range(amount)] + + # Create puzzles in parallel using process pool + with multiprocessing.Pool() as pool: + puzzles = pool.map( + TimeLockPuzzleFactory._create_puzzle_parallel, puzzle_params + ) + + return puzzles + + # Private Methods + # ------------------------------------------------------------------------------ + + @staticmethod + def _create_puzzle_parallel( + puzzle_params: Tuple[int, MPZ], + ) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + """Helper method to create a single puzzle tuple for multiprocessing. + + Args: + puzzle_params (Tuple[int, MPZ]): Tuple containing (bit_size, timing_parameter) + + Returns: + Tuple[TimeLockPuzzle, RSA, MPZ]: A tuple containing the puzzle, RSA instance, and solution + """ + bit_size, t = puzzle_params + factory = TimeLockPuzzleFactory(bit_size, t) + return factory.create_puzzle() diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/__init__.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/__init__.py new file mode 100644 index 0000000..33481c8 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/__init__.py @@ -0,0 +1,25 @@ +"""Time lock puzzle module.""" + +from .TimeLockPuzzle import TimeLockPuzzle +from .TimeLockPuzzleBuilder import TimeLockPuzzleBuilder +from .TimeLockPuzzleFactory import TimeLockPuzzleFactory +from .EfficientTimeLockPuzzleSolver import EfficientTimeLockPuzzleSolver +from .SequentialTimeLockPuzzleSolver import SequentialTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .abstract.ITimeLockPuzzleBuilder import ITimeLockPuzzleBuilder +from .abstract.ITimeLockPuzzleFactory import ITimeLockPuzzleFactory +from .abstract.IEfficientTimeLockPuzzleSolver import IEfficientTimeLockPuzzleSolver +from .abstract.ISequentialTimeLockPuzzleSolver import ISequentialTimeLockPuzzleSolver + +__all__ = [ + "TimeLockPuzzle", + "TimeLockPuzzleBuilder", + "TimeLockPuzzleFactory", + "EfficientTimeLockPuzzleSolver", + "SequentialTimeLockPuzzleSolver", + "ITimeLockPuzzle", + "ITimeLockPuzzleBuilder", + "ITimeLockPuzzleFactory", + "IEfficientTimeLockPuzzleSolver", + "ISequentialTimeLockPuzzleSolver", +] diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py new file mode 100644 index 0000000..f4b66df --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple +from ...mpc.types import MPZ +from ...rsa import RSA +from .ITimeLockPuzzle import ITimeLockPuzzle + + +class IEfficientTimeLockPuzzleSolver(ABC): + """Abstract base class defining the interface for an efficient time lock puzzle solver.""" + + @staticmethod + @abstractmethod + def solve(rsa: RSA, puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle efficiently using RSA private parameters. + + Args: + rsa (RSA): The RSA instance with private parameters + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ + pass + + @staticmethod + @abstractmethod + def solve_many(puzzles: List[Tuple[RSA, ITimeLockPuzzle]]) -> List[MPZ]: + """Solve multiple time lock puzzles in parallel using multiprocessing. + + Args: + puzzles: List of tuples containing (RSA, puzzle) pairs to solve + + Returns: + List[MPZ]: List of solutions in the same order as input puzzles + """ + pass diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py new file mode 100644 index 0000000..c8f6889 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ +from .ITimeLockPuzzle import ITimeLockPuzzle + + +class ISequentialTimeLockPuzzleSolver(ABC): + """Abstract base class defining the interface for a sequential time lock puzzle solver.""" + + @staticmethod + @abstractmethod + def solve(puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle sequentially without RSA private parameters. + + Args: + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py new file mode 100644 index 0000000..74b748d --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class ITimeLockPuzzle(ABC): + """Abstract base class defining the interface for a time lock puzzle implementation.""" + + @abstractmethod + def get_x(self) -> MPZ: + """Get the input value x. + + Returns: + MPZ: The input value x + """ + + @abstractmethod + def get_t(self) -> MPZ: + """Get the time parameter t. + + Returns: + MPZ: The time parameter t + """ + + @abstractmethod + def get_N(self) -> MPZ: + """Get the modulus N. + + Returns: + MPZ: The modulus N + """ diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py new file mode 100644 index 0000000..aa24405 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py @@ -0,0 +1,49 @@ +from abc import ABC, abstractmethod +from typing import Self +from ...mpc.types import MPZ +from ..TimeLockPuzzle import TimeLockPuzzle + + +class ITimeLockPuzzleBuilder(ABC): + """Abstract base class defining the interface for a time lock puzzle builder.""" + + @abstractmethod + def set_x(self, x: MPZ) -> Self: + """Set the input value x. + + Args: + x (MPZ): The input value + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def set_t(self, t: MPZ) -> Self: + """Set the time parameter t. + + Args: + t (MPZ): The time parameter + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def set_N(self, N: MPZ) -> Self: + """Set the modulus N. + + Args: + N (MPZ): The modulus + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def build(self) -> TimeLockPuzzle: + """Build the time lock puzzle. + + Returns: + TimeLockPuzzle: The constructed puzzle + """ diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py new file mode 100644 index 0000000..26bb910 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple +from ...mpc.types import MPZ +from ...rsa import RSA +from ..TimeLockPuzzle import TimeLockPuzzle + + +class ITimeLockPuzzleFactory(ABC): + """Abstract base class defining the interface for a time lock puzzle factory.""" + + @abstractmethod + def create_puzzle(self) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + """Create a new time lock puzzle with solution. + + Returns: + Tuple[TimeLockPuzzle, RSA, MPZ]: A tuple containing: + - The time lock puzzle + - The RSA instance used to create the puzzle + - The solution y + """ + + @abstractmethod + def create_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + """Create multiple time lock puzzles with solutions in parallel. + + Args: + amount (int): Number of puzzles to create + + Returns: + List[Tuple[TimeLockPuzzle, RSA, MPZ]]: A list of tuples, each containing: + - The time lock puzzle + - The RSA instance used to create the puzzle + - The solution y + """ diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/constants.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/constants.py new file mode 100644 index 0000000..acfd082 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/constants.py @@ -0,0 +1,5 @@ +"""Constants for time lock puzzle module.""" + +from ..mpc import MPC + +TWO = MPC.mpz(2) diff --git a/requester/src/app.ts b/requester/src/app.ts index bff7611..aad8e30 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -3,7 +3,7 @@ import { } from "ao-process-clients"; import { TransferToProviders } from "./extra"; -const RETRY_DELAY_MS = 3000; // 3 seconds +const RETRY_DELAY_MS = 1000; // 1 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; diff --git a/requester/src/extra.ts b/requester/src/extra.ts index 16547fe..fb6805c 100644 --- a/requester/src/extra.ts +++ b/requester/src/extra.ts @@ -10,7 +10,7 @@ const { spawn, message, result } = connect({ }); const TOKEN_PROCESS = "rPpsRk9Rm8_SJ1JF8m9_zjTalkv9Soaa_5U0tYUloeY" -const RAND_PROCESS = "ZBSQD_GeGUdQAiixxKy9Ag1rgJvJ_yFUGExwjW6mA7E" +const RAND_PROCESS = "8N08BvmC34q9Hxj-YS6eAOd_cSmYqGpezPPHUYWJBhg" export async function fetchMessageResult( messageID: string, processID: string From ee0f00a104c7abbfa3d97c672d4c57acedc1cda7 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 14 May 2025 13:55:14 -0400 Subject: [PATCH 54/80] V1 done --- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 45 ++++- orchestrator/src/containerManagment.ts | 32 +--- orchestrator/src/helperFunctions.ts | 114 +++++++++++- orchestrator/src/monitoring.ts | 237 +++++++++++++++++++++++++ requester/package.json | 2 +- requester/src/app.ts | 16 +- 8 files changed, 400 insertions(+), 50 deletions(-) create mode 100644 orchestrator/src/monitoring.ts diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 6988f2a..233a29a 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v0.4.68 # You can change this value to any version you want +export VERSION=v1.0.0 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index fdbc234..cede4e4 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^6.0.18", + "ao-process-clients": "^6.0.55", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 6eceef7..4ead594 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -2,9 +2,10 @@ import Docker from 'dockerode'; import AWS from 'aws-sdk'; import { connectWithRetry, setupDatabase } from './db_tools.js'; import Arweave from 'arweave'; -import { checkAndFetchIfNeeded, cleanupFulfilledEntries, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown } from './helperFunctions.js'; +import { checkAndFetchIfNeeded, cleanupFulfilledEntries, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown, initializeAutoUpdateTimer } from './helperFunctions.js'; import { monitorDockerContainers } from './containerManagment.js'; import logger, { LogLevel, Logger } from './logger'; +import { monitoring } from './monitoring'; export const docker = new Docker(); export const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); @@ -12,14 +13,14 @@ export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; export const DOCKER_MONITORING_TIME= 30000; -export const POLLING_INTERVAL_MS = 2500; //2.5 seconds +export const POLLING_INTERVAL_MS = 0; //0 second export const DATABASE_CHECK_TIME = 60000; //60 seconds -export const MINIMUM_ENTRIES = 1000; +export const MINIMUM_ENTRIES = 2500; export const DRYRUNTIMEOUT = 30000; // 30 seconds export const MAX_RETRIES = 10; export const RETRY_DELAY_MS = 10000; export const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds -export const UNCHAIN_VS_OFFCHAIN_MAX_DIF = 250; +export const UNCHAIN_VS_OFFCHAIN_MAX_DIF = 50; let PROVIDER_ID = ""; let pollingInProgress = false; @@ -76,7 +77,10 @@ async function polling(client: any) { const s1 = Date.now(); logger.debug(`${logId} Step 1 started.`); const openRequests = await getProviderRequests(PROVIDER_ID, logId); - stepTracking.step1 = { completed: true, timeTaken: Date.now() - s1 }; + const timeTaken = Date.now() - s1; + stepTracking.step1 = { completed: true, timeTaken }; + // Update monitoring with step timing + monitoring.updateStepTiming('step1', timeTaken); logger.debug(`${logId} Step 1: Open requests fetched. Time taken: ${stepTracking.step1.timeTaken}ms`); // Run Step 2, 3, and 4 concurrently @@ -85,14 +89,20 @@ async function polling(client: any) { const s2 = Date.now(); logger.debug(`${logId} Step 2 started.`); await processChallengeRequests(client, openRequests.activeChallengeRequests, logId); - stepTracking.step2 = { completed: true, timeTaken: Date.now() - s2 }; + const timeTaken = Date.now() - s2; + stepTracking.step2 = { completed: true, timeTaken }; + // Update monitoring with step timing + monitoring.updateStepTiming('step2', timeTaken); logger.debug(`${logId} Step 2 completed. Time taken: ${stepTracking.step2.timeTaken}ms`); })(), (async () => { const s3 = Date.now(); logger.debug(`${logId} Step 3 started.`); await processOutputRequests(client, openRequests.activeOutputRequests, logId); - stepTracking.step3 = { completed: true, timeTaken: Date.now() - s3 }; + const timeTaken = Date.now() - s3; + stepTracking.step3 = { completed: true, timeTaken }; + // Update monitoring with step timing + monitoring.updateStepTiming('step3', timeTaken); logger.debug(`${logId} Step 3 completed. Time taken: ${stepTracking.step3.timeTaken}ms`); })(), (async () => { @@ -101,16 +111,23 @@ async function polling(client: any) { //TODO enable this again later await cleanupFulfilledEntries(client, openRequests, logId); await checkAndFetchIfNeeded(client) - stepTracking.step4 = { completed: true, timeTaken: Date.now() - s4 }; + const timeTaken = Date.now() - s4; + stepTracking.step4 = { completed: true, timeTaken }; + // Update monitoring with step timing + monitoring.updateStepTiming('step4', timeTaken); logger.debug(`${logId} Step 4 completed. Time taken: ${stepTracking.step4.timeTaken}ms`); })(), ]); const totalTime = Date.now() - startTime; + // Update overall step timing in monitoring + monitoring.updateStepTiming('overall', totalTime); logger.info(`${logId} Polling cycle completed successfully. Total time taken: ${totalTime}ms`); } catch (error) { logger.error(`${logId} An error occurred during polling:`, error); + // Increment error count in monitoring + monitoring.incrementErrorCount(); } finally { pollingInProgress = false; // Reset flag after execution } @@ -124,10 +141,22 @@ async function run(): Promise { const client = await connectWithRetry(); await setupDatabase(client); + + // Get database entry count on startup + try { + const res = await client.query('SELECT COUNT(*) as count FROM time_lock_puzzles WHERE request_id IS NULL'); + const entriesAvailable = parseInt(res.rows[0].count, 10); + logger.info(`Initial database entry count: ${entriesAvailable}`); + } catch (error) { + logger.error("Error getting initial database count:", error); + } arweave.wallets.jwkToAddress(JSON.parse(process.env.WALLET_JSON!)).then((address) => { logger.info(`Provider ID: ${address}`); PROVIDER_ID = address; + + // Initialize auto-update timer for provider values + initializeAutoUpdateTimer(); }); // setInterval(async () => { diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts index c5d7d09..0182929 100644 --- a/orchestrator/src/containerManagment.ts +++ b/orchestrator/src/containerManagment.ts @@ -2,6 +2,7 @@ import { docker, DOCKER_NETWORK, ecs, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE, UN import AWS from 'aws-sdk'; import { dbConfig } from "./db_tools"; import logger from "./logger"; +import { monitoring } from "./monitoring"; export interface NetworkConfig { subnets: string[]; @@ -171,37 +172,6 @@ export async function getMoreRandom(currentCount: number) { } } -// Modified function to wait for ECS tasks to complete and remove them from tracking -async function monitorECSTasks(): Promise { - if (ongoingContainers.size === 0) return; - - const describeTasksResult = await ecs.describeTasks({ - cluster: process.env.ECS_CLUSTER_NAME || 'fargate-cluster', - tasks: Array.from(ongoingContainers) - }).promise(); - - describeTasksResult.tasks?.forEach(task => { - // Check capacity provider name to determine if it is running Fargate or Fargate Spot - const capacityProvider = task.capacityProviderName || 'Unknown'; - - logger.debug(`ECS task: ${task.taskArn}, Capacity Provider: ${capacityProvider}`); - - if (task.lastStatus === 'STOPPED') { - logger.info(`ECS task stopped: ${task.taskArn}`); - if (task.stoppedReason) { - logger.info(`Task stopped reason: ${task.stoppedReason}`); - if (task.stoppedReason.includes('Host EC2 instance termination')) { - spotInterruptions++; - logger.warn(`Spot instance reclaimed by AWS. Total interruptions so far: ${spotInterruptions}`); - } - } else { - logger.info('Task stopped due to normal completion.'); - } - ongoingContainers.delete(task.taskArn as string); - } - }); -} - // Function to wait for Docker containers to complete and remove them from tracking export async function monitorDockerContainers(): Promise { if (ongoingContainers.size === 0) return; diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 1a0adef..9d79b1d 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -3,11 +3,22 @@ import { Client } from "pg"; import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; import { getMoreRandom } from "./containerManagment"; import logger, { LogLevel } from "./logger"; +import { monitoring } from "./monitoring"; +import { setTimeout, setInterval } from 'timers'; let randomClientInstance: RandomClient | null = null; let lastInitTime: number = 0; const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds let current_onchain_random = - 10 + +// Cooldown tracking for updateAvailableValuesAsync +let lastUpdateTimestamp = 0; +let isUpdateOnCooldown = false; +let lastAutoUpdateTimestamp = 0; + +// Cooldown tracking for fulfillRandomChallenge and fulfillRandomOutput (per request ID) +const challengeCooldowns = new Map(); +const outputCooldowns = new Map(); let ongoingRequest = false; // Optional: Auto-reinitialize on a timer @@ -22,6 +33,10 @@ export async function getRandomClient(): Promise { logger.debug("Initializing RandomClient"); randomClientInstance = ((await RandomClient.defaultBuilder())) .withWallet(JSON.parse(process.env.WALLET_JSON!)) + .withAOConfig({ + CU_URL: "https://ur-pcu.randao.net", + MODE: "legacy" + }) .build(); lastInitTime = currentTime; logger.debug("RandomClient initialized"); @@ -352,13 +367,47 @@ export async function checkAndFetchIfNeeded(client: Client) { } } -export function updateAvailableValuesAsync(currentCount: number) { +export function updateAvailableValuesAsync(currentCount: number, forceUseOnchainValue: boolean = false) { + // If on cooldown, log and exit without updating + if (isUpdateOnCooldown) { + logger.debug(`Update skipped - on cooldown (${Math.floor((30000 - (Date.now() - lastUpdateTimestamp)) / 1000)}s remaining)`); + return; + } + return (async () => { try { - await (await getRandomClient()).updateProviderAvailableValues(currentCount); - logger.info(`Updated provider values to ${currentCount}`); + // Set cooldown status + isUpdateOnCooldown = true; + lastUpdateTimestamp = Date.now(); + + // Determine which value to use + const valueToUpdate = forceUseOnchainValue ? current_onchain_random : currentCount; + + // Get monitoring data including system specs and performance metrics + const monitoringData = await monitoring.getMonitoringData(); + + // Send the count and monitoring data to the AO process + await (await getRandomClient()).updateProviderAvailableValues(valueToUpdate, monitoringData); + logger.info(`Updated provider values to ${valueToUpdate}${forceUseOnchainValue ? ' (using on-chain value)' : ''}`); + + // Track last auto-update time if this was a forced update + if (forceUseOnchainValue) { + lastAutoUpdateTimestamp = Date.now(); + } + + // Set timeout to release cooldown after 30 seconds + setTimeout(() => { + isUpdateOnCooldown = false; + logger.debug('Update cooldown period ended'); + }, 30000); } catch (error) { logger.error("Failed to update provider values:", error); + monitoring.incrementErrorCount(); + // Release cooldown on error after 5 seconds to allow retry + setTimeout(() => { + isUpdateOnCooldown = false; + logger.debug('Update cooldown period ended (after error)'); + }, 5000); } })(); } @@ -377,7 +426,16 @@ export async function getProviderAvailableRandomValues(PROVIDER_ID: string): Pro // Function to post VDF challenge (fetches dbId dynamically) async function fulfillRandomChallenge(client: Client, requestId: string, parentLogId: string): Promise { + // Check if this request ID is on cooldown + if (challengeCooldowns.get(requestId)) { + logger.debug(`${parentLogId} Skipping challenge for request ID ${requestId} - on cooldown`); + return; + } + try { + // Mark this request ID as being processed to prevent duplicates + challengeCooldowns.set(requestId, true); + // Fetch the necessary details from the database using requestId const res = await client.query( `SELECT id, modulus, x @@ -404,14 +462,31 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL } }); logger.info(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); + + // Set a timeout to release the cooldown after 1 second + setTimeout(() => { + challengeCooldowns.delete(requestId); + logger.debug(`${parentLogId} Challenge cooldown released for request ID: ${requestId}`); + }, 1000); } catch (error) { logger.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); + // Release the cooldown immediately on error to allow retry + challengeCooldowns.delete(requestId); } } // Function to post VDF output and proof async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { + // Check if this request ID is on cooldown + if (outputCooldowns.get(requestId)) { + logger.debug(`${parentLogId} Skipping output for request ID ${requestId} - on cooldown`); + return; + } + try { + // Mark this request ID as being processed to prevent duplicates + outputCooldowns.set(requestId, true); + // Fetch the output and proof from the database using the requestId const res = await client.query( `SELECT @@ -449,18 +524,49 @@ async function fulfillRandomOutput(client: Client, requestId: string, parentLogI } }); logger.info(`${parentLogId} Proof posted for request ID: ${requestId}`); + + // Set a timeout to release the cooldown after 1 second + setTimeout(() => { + outputCooldowns.delete(requestId); + logger.debug(`${parentLogId} Output cooldown released for request ID: ${requestId}`); + }, 1000); } catch (error) { logger.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); + // Release the cooldown immediately on error to allow retry + outputCooldowns.delete(requestId); } } +// Initialize automatic update timer +export function initializeAutoUpdateTimer() { + logger.info('Starting auto update timer for provider values'); + lastAutoUpdateTimestamp = Date.now(); // Initialize timer start + + // Set up interval check that runs every minute + setInterval(() => { + const timeSinceLastUpdate = Date.now() - lastAutoUpdateTimestamp; + const tenMinutesInMs = 10 * 60 * 1000; + + if (timeSinceLastUpdate >= tenMinutesInMs) { + logger.info(`Auto-update triggered - ${Math.floor(timeSinceLastUpdate / 60000)} minutes since last update`); + updateAvailableValuesAsync(0, true); // Force use of on-chain value + } + }, 60000); // Check every minute +} + export async function shutdown() { try { const randomClient = await getRandomClient(); - const message = await randomClient.updateProviderAvailableValues(0); + + // Get monitoring data for final update + const monitoringData = await monitoring.getMonitoringData(); + + // Set provider available values to 0 and include final monitoring data + const message = await randomClient.updateProviderAvailableValues(0, monitoringData); logger.info(String(message)); // Convert message to string logger.info(`Updated provider values to 0`); } catch (error) { logger.error("Failed to update provider values:", error); + monitoring.incrementErrorCount(); } } diff --git a/orchestrator/src/monitoring.ts b/orchestrator/src/monitoring.ts new file mode 100644 index 0000000..f65fe07 --- /dev/null +++ b/orchestrator/src/monitoring.ts @@ -0,0 +1,237 @@ +import os from 'os'; +import fs from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import crypto from 'crypto'; +import { version } from '../package.json'; +import logger from './logger'; + +const execAsync = promisify(exec); + +// Moving averages for each step +interface StepTimings { + step1: number; + step2: number; + step3: number; + step4: number; + overall: number; +} + +// Performance counters +interface NetworkCounters { + rxBytes: number; + txBytes: number; + rxPackets: number; + txPackets: number; +} + +// Class to manage all monitoring data +export class MonitoringService { + private static instance: MonitoringService; + private machineId: string; + + // Metrics tracking + private stepTimings: StepTimings = { + step1: 0, + step2: 0, + step3: 0, + step4: 0, + overall: 0 + }; + + private totalStepSamples: { [key: string]: number } = { + step1: 0, + step2: 0, + step3: 0, + step4: 0, + overall: 0 + }; + + private errorCount: number = 0; + private previousNetworkStats: NetworkCounters | null = null; + + private constructor() { + this.machineId = this.generateMachineId(); + + // Initialize network stats + this.updateNetworkStats().catch(err => + logger.error('Failed to initialize network stats:', err) + ); + } + + public static getInstance(): MonitoringService { + if (!MonitoringService.instance) { + MonitoringService.instance = new MonitoringService(); + } + return MonitoringService.instance; + } + + private generateMachineId(): string { + try { + // Using stable hardware identifiers that won't change between restarts + // but will be unique to physical/virtual machines + + // Get CPU information which is generally the same across containers on same host + const cpuModel = os.cpus()[0]?.model || ''; + const cpuSpeed = os.cpus()[0]?.speed || 0; + const totalCores = os.cpus().length; + + // System memory size is usually fixed for a machine + const totalMemory = os.totalmem(); + + // Combine all available identifiers with more hardware specs + const hwInfo = `${cpuModel}-${cpuSpeed}-${totalCores}-${totalMemory}-${os.platform()}-${os.arch()}`; + + // Generate a shorter hash (first 16 chars of SHA-256) for easier identification while maintaining uniqueness + return crypto.createHash('sha256').update(hwInfo).digest('hex').substring(0, 16); + } catch (error) { + logger.error('Error generating machine ID:', error); + // Fallback to a less reliable but still somewhat useful ID + return crypto.createHash('sha256').update(os.hostname() + os.platform()).digest('hex').substring(0, 16); + } + } + + private async updateNetworkStats(): Promise { + try { + let networkStats: NetworkCounters = { + rxBytes: 0, + txBytes: 0, + rxPackets: 0, + txPackets: 0 + }; + + if (process.platform === 'linux') { + // Linux - read from /proc/net/dev + const netDev = await fs.promises.readFile('/proc/net/dev', 'utf8'); + const interfaces = netDev.split('\n').filter(line => + line.includes(':') && !line.includes('lo:') + ); + + for (const intf of interfaces) { + const parts = intf.trim().split(/\s+/); + networkStats.rxBytes += parseInt(parts[1] || '0', 10); + networkStats.rxPackets += parseInt(parts[2] || '0', 10); + networkStats.txBytes += parseInt(parts[9] || '0', 10); + networkStats.txPackets += parseInt(parts[10] || '0', 10); + } + } else if (process.platform === 'win32') { + // Windows - use PowerShell to get network stats + const { stdout } = await execAsync( + 'powershell "Get-NetAdapterStatistics | Select-Object ReceivedBytes,ReceivedPackets,SentBytes,SentPackets | ConvertTo-Json"' + ); + + try { + const stats = JSON.parse(stdout); + const adapters = Array.isArray(stats) ? stats : [stats]; + for (const adapter of adapters) { + networkStats.rxBytes += adapter.ReceivedBytes || 0; + networkStats.rxPackets += adapter.ReceivedPackets || 0; + networkStats.txBytes += adapter.SentBytes || 0; + networkStats.txPackets += adapter.SentPackets || 0; + } + } catch (e) { + logger.error('Failed to parse network stats:', e); + } + } + + this.previousNetworkStats = networkStats; + return networkStats; + } catch (error) { + logger.error('Error getting network stats:', error); + return { + rxBytes: 0, + txBytes: 0, + rxPackets: 0, + txPackets: 0 + }; + } + } + + public updateStepTiming(step: string, timeTaken: number): void { + if (step in this.stepTimings) { + // Calculate running average + const currentSamples = this.totalStepSamples[step]; + const currentAvg = this.stepTimings[step as keyof StepTimings]; + + // Update running average + this.stepTimings[step as keyof StepTimings] = + (currentAvg * currentSamples + timeTaken) / (currentSamples + 1); + this.totalStepSamples[step]++; + } + } + + public incrementErrorCount(): void { + this.errorCount++; + } + + public async getMonitoringData(): Promise { + // Get real-time system metrics + const cpuInfo = os.cpus(); + const loadAvg = os.loadavg(); + const totalMemory = os.totalmem(); + const freeMemory = os.freemem(); + const usedMemoryPercent = Math.round((1 - freeMemory / totalMemory) * 100); + + // Get disk info - only used percent + let diskUsedPercent = 0; + + try { + if (process.platform === 'linux') { + const { stdout } = await execAsync('df -h / --output=pcent'); + const lines = stdout.trim().split('\n'); + if (lines.length > 1) { + diskUsedPercent = parseInt(lines[1].trim().replace('%', ''), 10); + } + } else if (process.platform === 'win32') { + const { stdout } = await execAsync( + 'powershell "Get-Volume | Where-Object {$_.DriveLetter -eq \'C\'} | Select-Object @{Name=\'UsedPercent\';Expression={100 - (($_.SizeRemaining / $_.Size) * 100)}} | ConvertTo-Json"' + ); + + try { + const diskData = JSON.parse(stdout); + diskUsedPercent = Math.round(diskData.UsedPercent || 0); + } catch (e) { + logger.error('Failed to parse disk info:', e); + } + } + } catch (error) { + logger.error('Error getting disk info:', error); + } + + // Get updated network stats + const networkStats = await this.updateNetworkStats(); + + // Construct monitoring data object + const monitoringData = { + providerVersion: version, + + systemSpecs: { + arch: os.arch(), + cpuCount: cpuInfo.length, + memoryTotalBytes: totalMemory, + token: this.machineId + }, + + performance: { + loadAverage: loadAvg, + memoryUsedPercent: usedMemoryPercent, + diskUsedPercent: diskUsedPercent, + network: networkStats + }, + + executionMetrics: { + stepTimingsMs: this.stepTimings + }, + + health: { + errors: this.errorCount, + status: this.errorCount > 10 ? "degraded" : "healthy" + } + }; + + return JSON.stringify(monitoringData); + } +} + +// Export a singleton instance +export const monitoring = MonitoringService.getInstance(); diff --git a/requester/package.json b/requester/package.json index 1cda5f8..adf0d48 100644 --- a/requester/package.json +++ b/requester/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "@permaweb/aoconnect": "^0.0.78", - "ao-process-clients": "^6.0.18", + "ao-process-clients": "^6.0.54", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index aad8e30..bb458b8 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -3,7 +3,7 @@ import { } from "ao-process-clients"; import { TransferToProviders } from "./extra"; -const RETRY_DELAY_MS = 1000; // 1 seconds +const RETRY_DELAY_MS = 5000; // 1 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; @@ -17,6 +17,7 @@ let lastProviderRefresh = 0; // // MU_URL: "https://mu.ao-testnet.xyz", // // CU_URL: "https://cu.ao-testnet.xyz", // GATEWAY_URL: "https://arweave.net", +// MODE: "legacy" // }; let randomClientInstance: RandomClient | null = null; @@ -25,7 +26,14 @@ async function getRandomClient(): Promise { if (!randomClientInstance) { randomClientInstance = ((await RandomClient.defaultBuilder())) - //.withAOConfig(AO_CONFIG) + .withAOConfig({ + MU_URL: "https://ur-mu.randao.net", + CU_URL: "https://ur-cu.randao.net", + // MU_URL: "https://mu.ao-testnet.xyz", + // CU_URL: "https://cu.ao-testnet.xyz", + GATEWAY_URL: "https://arweave.net", + MODE: "legacy" + }) .withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)) .build(); } @@ -125,8 +133,8 @@ async function main() { const callbackId = `callback-${Date.now()}`; const { providers, count } = await getRandomProviders(randclient); console.log(`Selected ${count} providers:`, providers); - //await randclient.createRequest(providers, count, callbackId); - await TransferToProviders(providers, callbackId) + await randclient.createRequest(providers, count, callbackId); + //await TransferToProviders(providers, callbackId) //await randclient.createRequest(["X1tqliRkKnClhVQ4aIeyuOaPTzr5PfnxqAoSdpTzZy8"], 1, "123"); totalRandomCalled++; console.log("Random request initiated. Awaiting request ID in open requests..."); From 55db2a5555bc083827ee5e397c5dd0e57531f0f5 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 14 May 2025 13:55:32 -0400 Subject: [PATCH 55/80] V1 done2 --- docker-compose/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index a76a590..c263808 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v0.4.68 + image: randao/orchestrator:v1.0.0 depends_on: postgres: condition: service_healthy From e8ae0b35d80c2717fa7ea57ffa99ba3ad15a3b34 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 14 May 2025 17:37:46 -0400 Subject: [PATCH 56/80] updated docs --- README.md | 156 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1f3cd0a..5de4cd9 100644 --- a/README.md +++ b/README.md @@ -207,29 +207,155 @@ If you encounter issues with your provider, here are some common problems and so ## Frequently Asked Questions ### What is a randomness provider? -A randomness provider generates verifiable random numbers that are used in various decentralized applications on the blockchain. These random numbers are crucial for fair and transparent operation of many applications. +A provider generates **verifiable random numbers** for decentralized applications using a commit-reveal mechanism with timelock puzzles. These random values are essential for fairness in blockchain-based systems. ### How do I earn rewards? -You earn rewards by providing random values to users who request them. The rewards depend on your provider's performance, reliability, and response time. +Once staked and online, you earn RNG-Test tokens based on: +- Your uptime +- Response speed +- Reliability in puzzle submission + +> ⚠️ You **must stake RNG-Test tokens** through the web UI and keep your node online to earn rewards. + +### How do I set up a provider? +1. Clone the repo. +2. Create a new wallet in your browser and send it RNG-Test tokens. +3. Stake using the site UI. +4. Place the wallet's **private key JSON** into your `.env` file. +5. Run `docker-compose up` from the repo directory. + +Your provider will start serving randomness as soon as it's recognized. + +### What are the minimum system requirements? +Minimum: +- 2 CPU cores +- 4 GB RAM +- Stable internet connection +- SSD or fast flash storage (not HDD) + +Recommended: +- 4 CPU cores +- 8 GB RAM + +Network stability and uptime matter more than CPU performance. + +### Can I run this on a Raspberry Pi? +Yes. A Raspberry Pi 4 or 5 works great. This service is very lightweight, and Pis are ideal for 24/7 uptime with low power usage. + +> 💡 Use fast external storage (USB SSD) if possible, and make sure your internet is reliable. + +### I rebooted and now my local and on-chain values don’t match. Is that okay? +Yes. This is normal — local and on-chain values can differ slightly due to how often each updates. As long as: +- There are **no errors** +- The on-chain “Available Random” is **positive** + +You’re fine. The system will reconcile automatically over time. + +### My `.env` file might be broken — how do I check it? +Check the following: +- The file matches `.env.example` +- You correctly pasted your **wallet's private key JSON** +- There are no formatting issues (e.g. missing quotes or equals signs) + +If unsure, restart your node with: +```bash +docker-compose down +docker-compose up +``` + +If it still fails, reach out in Discord. + +### I staked tokens but I get “Failed to stake tokens.” What should I do? +- Create a fresh wallet using the browser interface +- Send it RNG-Test tokens +- Stake via the site UI (you should see your balance) +- Paste the wallet's private key JSON into `.env` +- Restart the node + +If it still fails after confirming the above, open a support ticket in Discord. + +### My node is running but DB size, on-chain, and local values are all 0. What’s wrong? +Most likely causes: +- `.env` is misconfigured or contains an invalid wallet JSON +- The puzzle generator Docker image failed to pull + +Try: +- Checking `.env` for typos +- Pulling the image manually: `docker pull randao/puzzle-gen:v0.1.1` +- Restarting with `docker-compose down && docker-compose up` + +### My node is running, but the site doesn’t recognize me as a provider. What’s missing? +Two things must happen: +1. You must **stake** using the browser wallet +2. The `.env` file must contain the exact wallet JSON used for staking + +If either of these is missing or mismatched, the AO network won’t register you as an active provider. + +### Port 3000 is already in use — can I change it? +No need. The provider runs inside a **Docker virtual network** using port 3000 internally. It won't conflict with other services on your host machine, even if they use port 3000. + +> Your host system and other apps will not be affected. + +### Getting error: `No such image: randao/puzzle-gen:v0.1.1` — how do I fix this? +Run this manually: +```bash +docker pull randao/puzzle-gen:v0.1.1 +``` +This will fetch the image in case there was a permissions issue or the auto-pull failed. + +Once done, restart with `docker-compose up`. + +### Can I run this node on the same VPS as my Ar.io Gateway node? +Yes — this is a great combo. There are **no known conflicts** when running them together. The two services don’t compete for ports or storage and run happily side-by-side. + +> We are working on adding this provider as an optional Ar.io sidecar soon. + +### My node shut down and “Random Available” shows -2. What does that mean? +Negative values mean your provider is **offline** or **disabled**: +- `-1`: You manually shut it down +- `-2`: AO disabled you for being too slow +- `-3`: Disabled by the team (rare) + +Your provider won’t auto-recover. Go to the provider site and **toggle it back on manually**. + +### What does “Random Available” mean? +- **Positive number**: You’re active and have this much randomness ready to serve +- **0**: You shut down gracefully +- **Negative number**: You’ve been disabled (see above) + +Random must be generated **in advance** via timelock puzzles. That’s what’s reflected in this value. + +### What are the minimum requirements to run the node smoothly? +You need: +- **Minimum**: 2 CPU cores, 4 GB RAM +- **Recommended**: 4 CPU cores, 8 GB RAM +- **Storage**: SSD or fast flash storage only (no HDDs) + +The main requirement is **stable internet** and **high uptime**, not processing power. + +### How many tokens do I need to become a full validator? +You need **10,000 RNG-Test tokens** to become a validator. These can be claimed cheaply from the faucet. + +> Holding tokens alone does not qualify for airdrops — you must provide randomness (i.e. increase your served count). ### What happens if my provider goes offline? -If your provider goes offline without following the graceful shutdown procedure, you may be penalized (slashed). Always follow the [Graceful Shutdown](#graceful-shutdown) procedure before taking your provider offline. +- If you shut it down **gracefully** (SIGTERM or UI), it sets `availableRandom` to 0 and avoids slashing. +- If you shut it down **abruptly**, you may be slashed or marked inactive. -### How much can I earn as a provider? -Earnings depend on network demand, your provider's performance, and the amount you have staked. Better-performing providers with higher stakes tend to earn more. +Always use the proper shutdown button or `CTRL+C` so the system knows you're offline safely. ### Can I run multiple providers? -Yes, you can run multiple providers. Each provider needs its own unique wallet and must be staked separately. +Yes. Each provider: +- Needs a **unique wallet** +- Must be **staked separately** +- Requires its own `.env` file and Docker instance -### What is the recommended hardware for optimal performance? -While the minimum requirements are 4GB RAM and 2 CPU cores, we recommend at least 8GB RAM and 4 CPU cores for optimal performance. +You can scale across multiple servers or devices. ### Do I need technical knowledge to run a provider? -Basic familiarity with command line operations and Docker is helpful, but the quickstart guide is designed to be accessible even to those with limited technical experience. - -### How often do I need to update my provider? -Updates will be announced in the community channels. We recommend keeping your provider updated to the latest version for optimal performance and security. - ---- +Not much. If you can: +- Copy and paste into a terminal +- Edit a `.env` file +- Run Docker -Thank you for contributing to the network's success! +...you’re good to go. The setup is beginner-friendly and we offer full support via Discord. From 8766661d8cd6082531348806be304168706b0a0b Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 14 May 2025 17:42:59 -0400 Subject: [PATCH 57/80] updated docs2 --- README.md | 102 ++++++++++++++++++++++++++---------------------------- 1 file changed, 49 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 5de4cd9..144bcf3 100644 --- a/README.md +++ b/README.md @@ -42,16 +42,16 @@ docker-compose up -d That's it! Your node is now running and will start generating randomness for the network. -**Need help?** Check the [Troubleshooting](#troubleshooting) section or [Frequently Asked Questions](#frequently-asked-questions) below. +**Need help?** Check the [Frequently Asked Questions](#frequently-asked-questions) below. --- ## How It Works Your provider performs 3 main functions: -1. It creates and stores random values that others can request -2. It responds when someone requests a random value -3. It submits final verified random values to the blockchain +1. It creates and stores crytographic time lock puzzles to be used for random entropy generation +2. It responds with the puzzle, then the answer when someone requests a random value +3. It keeps the AO proccess informed on its random values and status so users can see if its availible or not The better your provider performs these functions, the more rewards you'll receive. Providers with faster response times earn more! @@ -64,7 +64,6 @@ To run a node, you'll need: - At least 2 CPU cores - Reliable internet connection -**Note:** These requirements may increase over time as the network grows. --- @@ -155,54 +154,6 @@ After maintenance is complete and your provider is back online: --- -## Troubleshooting - -If you encounter issues with your provider, here are some common problems and solutions: - -### "Provider Not Found" Error -- Ensure your provider is properly staked at ar://randao -- Wait for blockchain confirmation (it may take some time for your stake to be recognized) -- Check your wallet configuration in the `.env` file - -### Network Connectivity Issues -- If your provider can't connect to the network, check your internet connection -- Check your firewall settings to ensure the required ports are open -- Wait for network conditions to improve before attempting to restart - -### Slow or Unresponsive Provider -- Check system resources to ensure your host has sufficient CPU and memory -- Monitor the logs for any error messages or warnings: - ```bash - docker-compose logs -f - ``` -- If the puzzle generator is struggling, consider scaling up your hardware - -### General Issues -- Try restarting the containers: - ```bash - docker-compose restart - ``` -- For more persistent issues, you can try a full reset: - ```bash - docker-compose down - docker-compose up -d - ``` -- Ensure your container has the latest version: - ```bash - docker-compose pull - docker-compose down - docker-compose up -d - ``` - -### Database Issues -- If the database container fails to start, check logs for specific errors: - ```bash - docker-compose logs db - ``` -- Ensure the database password in your `.env` file doesn't contain special characters that need escaping -- Verify database volume permissions if running on Linux - ---- ## Frequently Asked Questions @@ -359,3 +310,48 @@ Not much. If you can: - Run Docker ...you’re good to go. The setup is beginner-friendly and we offer full support via Discord. + +### I'm seeing a "Provider Not Found" error. What should I do? +- Make sure your wallet is **staked** via the provider site: `ar://randao_providers` +- Wait for the blockchain to confirm your stake (this may take a few minutes) +- Check that your `.env` file contains the **correct wallet private key JSON** + +### My provider can't connect to the network. How can I fix this? +- Check your **internet connection** +- Make sure your **firewall isn't blocking** outgoing connections or Docker traffic +- If the issue is due to network instability, **wait and try restarting later** + +### My provider is slow or unresponsive. What’s going on? +- Confirm your host has **enough CPU and memory** +- Check real-time logs: + ```bash + docker-compose logs -f + ``` +- If the puzzle generator is stalling, consider upgrading your hardware or verifying Docker is pulling the right image + +### I’m having general issues. How can I restart or reset? +First, try a clean restart: +```bash +docker-compose restart +``` + +If problems persist, do a full reset: +```bash +docker-compose down +docker-compose up -d +``` + +To update to the latest version: +```bash +docker-compose pull +docker-compose down +docker-compose up -d +``` + +### My database container won’t start. How do I debug this? +- Check logs: + ```bash + docker-compose logs db + ``` +- Make sure your `.env` has a database password that **doesn’t contain special characters** (some need escaping) +- If using Linux, **check the volume permissions** on your database folder From 8ac08f77202e37a68e6d8e0f8d104ca4138d94f1 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 18 May 2025 10:33:00 -0400 Subject: [PATCH 58/80] slight upgrade to API --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 4 +- orchestrator/src/containerManagment.ts | 109 ++++++++----------------- orchestrator/src/helperFunctions.ts | 40 +++++++-- requester/package.json | 2 +- requester/src/app.ts | 2 +- 8 files changed, 71 insertions(+), 92 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index c263808..0ec1bad 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 5 orchestrator: - image: randao/orchestrator:v1.0.0 + image: randao/orchestrator:v1.0.1 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 233a29a..131d15f 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.0 # You can change this value to any version you want +export VERSION=v1.0.1 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index cede4e4..fb85e27 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^6.0.55", + "ao-process-clients": "^6.0.58", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 4ead594..679626b 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,5 +1,4 @@ import Docker from 'dockerode'; -import AWS from 'aws-sdk'; import { connectWithRetry, setupDatabase } from './db_tools.js'; import Arweave from 'arweave'; import { checkAndFetchIfNeeded, cleanupFulfilledEntries, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown, initializeAutoUpdateTimer } from './helperFunctions.js'; @@ -8,14 +7,13 @@ import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; export const docker = new Docker(); -export const ecs = new AWS.ECS({ region: process.env.AWS_REGION || 'us-east-1' }); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; export const DOCKER_MONITORING_TIME= 30000; export const POLLING_INTERVAL_MS = 0; //0 second export const DATABASE_CHECK_TIME = 60000; //60 seconds -export const MINIMUM_ENTRIES = 2500; +export const MINIMUM_ENTRIES = 5000; export const DRYRUNTIMEOUT = 30000; // 30 seconds export const MAX_RETRIES = 10; export const RETRY_DELAY_MS = 10000; diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts index 0182929..d866d56 100644 --- a/orchestrator/src/containerManagment.ts +++ b/orchestrator/src/containerManagment.ts @@ -1,87 +1,18 @@ -import { docker, DOCKER_NETWORK, ecs, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; -import AWS from 'aws-sdk'; +import { docker, DOCKER_NETWORK, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE } from "./app"; import { dbConfig } from "./db_tools"; import logger from "./logger"; -import { monitoring } from "./monitoring"; + export interface NetworkConfig { subnets: string[]; securityGroups: string[]; } -let spotInterruptions = 0; // Global variables to track polling status let pulledDockerimage = false; let pullingImagePromise: Promise | null = null; // Add at the top-level scope (module-global) -// Cache for network configuration -let cachedNetworkConfig: NetworkConfig | null = null; const ongoingContainers = new Set(); // Track container IDs of running Docker containers -export async function getNetworkConfig(ecs: AWS.ECS): Promise { - // Get the task definition to extract network configuration - const taskDef = await ecs.describeTaskDefinition({ taskDefinition: 'vdf-job' }).promise(); - - // Get the service to extract network configuration - const services = await ecs.listServices({ cluster: process.env.ECS_CLUSTER_NAME }).promise(); - const serviceArn = services.serviceArns?.find(arn => arn.includes('vdf-job-service')); - - if (!serviceArn) { - throw new Error('VDF job service not found'); - } - - const service = await ecs.describeServices({ - cluster: process.env.ECS_CLUSTER_NAME, - services: [serviceArn] - }).promise(); - - const networkConfig = service.services?.[0]?.networkConfiguration?.awsvpcConfiguration; - - if (!networkConfig?.subnets || !networkConfig?.securityGroups) { - throw new Error('Network configuration not found'); - } - - return { - subnets: networkConfig.subnets, - securityGroups: networkConfig.securityGroups - }; -} - -export async function launchVDFTask( - ecs: AWS.ECS, - networkConfig: NetworkConfig, - random_per_vdf: number, -): Promise { - const result = await ecs.runTask({ - cluster: process.env.ECS_CLUSTER_NAME, - taskDefinition: 'vdf-job', - capacityProviderStrategy: [ - { - capacityProvider: 'FARGATE_SPOT', - weight: 1, - }, - ], - networkConfiguration: { - awsvpcConfiguration: { - subnets: networkConfig.subnets, - securityGroups: networkConfig.securityGroups, - assignPublicIp: 'ENABLED', - }, - }, - count: 1, - overrides: { - containerOverrides: [ - { - name: 'vdf_job_container', // Must match the container name in the task definition - command: ['sh', '-c', `python3 main.py ${random_per_vdf}`], - }, - ], - }, - }).promise(); - - const taskArn = result.tasks?.[0]?.taskArn; - return taskArn || null; -} - export async function triggerTimePuzzleJobPod(randomCount: number): Promise { const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; @@ -150,33 +81,44 @@ export async function triggerTimePuzzleJobPod(randomCount: number): Promise 0) { logger.info("A puzzle-gen container is already running. Skipping new container launch."); return null; } - logger.info(`Spawning a single container to generate ${entriesNeeded} random values.`); + logger.info(`Spawning a single container to generate ${randomToGenerate} random values.`); try { - const jobId = await triggerTimePuzzleJobPod(entriesNeeded); + const jobId = await triggerTimePuzzleJobPod(randomToGenerate); if (jobId) { logger.info(`Job triggered: ${jobId}`); ongoingContainers.add(jobId); + return jobId; } } catch (error) { logger.error('Error triggering job pod:', error); } + + return null; } +// Import the function to reset the generation flag from helperFunctions +import { resetOngoingRandomGeneration } from './helperFunctions.js'; + // Function to wait for Docker containers to complete and remove them from tracking export async function monitorDockerContainers(): Promise { if (ongoingContainers.size === 0) return; logger.verbose(`Monitoring ${ongoingContainers.size} Docker containers`); + let containersRemoved = false; for (const containerId of ongoingContainers) { try { @@ -192,6 +134,14 @@ export async function monitorDockerContainers(): Promise { await container.remove({ force: true }); // Force removal to avoid "in progress" errors logger.info(`Docker container removed: ${containerId}`); ongoingContainers.delete(containerId); + containersRemoved = true; + + // Check exit code to log success or failure + if (containerInfo.State.ExitCode === 0) { + logger.info('Container completed successfully'); + } else { + logger.warn(`Container exited with non-zero code: ${containerInfo.State.ExitCode}`); + } } catch (removeError) { if (isDockerError(removeError) && removeError.statusCode === 409) { // Error 409 means removal is in progress, so skip this container for now @@ -205,8 +155,15 @@ export async function monitorDockerContainers(): Promise { } catch (error) { logger.error(`Error inspecting Docker container ${containerId}:`, error); ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) + containersRemoved = true; } } + + // If all containers are removed, reset the ongoingRandomGeneration flag + if (ongoingContainers.size === 0 && containersRemoved) { + logger.info('All random generation containers finished. Resetting generation flag.'); + resetOngoingRandomGeneration(); + } } // Helper function to type guard Docker errors diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 9d79b1d..6b802bf 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -19,7 +19,15 @@ let lastAutoUpdateTimestamp = 0; // Cooldown tracking for fulfillRandomChallenge and fulfillRandomOutput (per request ID) const challengeCooldowns = new Map(); const outputCooldowns = new Map(); -let ongoingRequest = false; +// Track whether there's an ongoing random generation request +let ongoingRandomGeneration = false; +const MAX_RANDOM_PER_REQUEST = 100; // Maximum number of random values to generate in a single request + +// Function to reset the ongoingRandomGeneration flag +export function resetOngoingRandomGeneration() { + ongoingRandomGeneration = false; + logger.info('Random generation flag reset. System ready for new random generation requests.'); +} // Optional: Auto-reinitialize on a timer setInterval(() => { @@ -34,7 +42,7 @@ export async function getRandomClient(): Promise { randomClientInstance = ((await RandomClient.defaultBuilder())) .withWallet(JSON.parse(process.env.WALLET_JSON!)) .withAOConfig({ - CU_URL: "https://ur-pcu.randao.net", + CU_URL: "https://ur-cu.randao.net", MODE: "legacy" }) .build(); @@ -307,6 +315,12 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri // Function to check and fetch database entries as needed export async function checkAndFetchIfNeeded(client: Client) { + // First check if a random generation is already in progress + if (ongoingRandomGeneration) { + logger.info('A random generation process is already running. Skipping new request.'); + return; + } + try { // Query current count of usable DB entries const res = await client.query( @@ -352,21 +366,31 @@ export async function checkAndFetchIfNeeded(client: Client) { logger.debug(`Not updating onchain values to avoid unneeded onchain messages. When difference is over ${UNCHAIN_VS_OFFCHAIN_MAX_DIF} an update will happen`); } } - if (ongoingRequest) return; // Prevent redundant operations // Check if more entries are needed if (currentCount >= MINIMUM_ENTRIES) return; - logger.info(`Less than ${MINIMUM_ENTRIES} entries found. Fetching more entries...`); - getMoreRandom(currentCount); - ongoingRequest = true; + + // Set the flag to prevent concurrent random generation + ongoingRandomGeneration = true; + + // Calculate how many random values we need + const entriesNeeded = MINIMUM_ENTRIES - currentCount; + // Limit to MAX_RANDOM_PER_REQUEST + const randomToGenerate = Math.min(entriesNeeded, MAX_RANDOM_PER_REQUEST); + + logger.info(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${randomToGenerate} entries (out of ${entriesNeeded} needed)...`); + + // Start the random generation with the calculated amount + await getMoreRandom(currentCount, randomToGenerate); } catch (error) { logger.error('Error during check and fetch:', error); - } finally { - ongoingRequest = false; // Allow future operations + ongoingRandomGeneration = false; // Reset the flag on error } + // Note: we don't reset ongoingRandomGeneration here - it will be reset by the container monitoring } + export function updateAvailableValuesAsync(currentCount: number, forceUseOnchainValue: boolean = false) { // If on cooldown, log and exit without updating if (isUpdateOnCooldown) { diff --git a/requester/package.json b/requester/package.json index adf0d48..94e7566 100644 --- a/requester/package.json +++ b/requester/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "@permaweb/aoconnect": "^0.0.78", - "ao-process-clients": "^6.0.54", + "ao-process-clients": "^6.0.58", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index bb458b8..453f415 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -1,7 +1,7 @@ import { RandomClient, } from "ao-process-clients"; -import { TransferToProviders } from "./extra"; +//import { TransferToProviders } from "./extra"; const RETRY_DELAY_MS = 5000; // 1 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes From 34cebc13751685b290b466011aa35815457ee477 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 19 May 2025 22:55:51 -0400 Subject: [PATCH 59/80] 1.0.2 --- docker-compose/docker-compose.yml | 12 +- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 51 ++---- orchestrator/src/containerManagment.ts | 13 +- orchestrator/src/helperFunctions.ts | 208 +++++++++---------------- 6 files changed, 111 insertions(+), 177 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 0ec1bad..d930bd6 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -16,9 +16,14 @@ services: interval: 10s timeout: 5s retries: 5 + logging: + driver: json-file + options: + max-size: "100m" + max-file: "5" orchestrator: - image: randao/orchestrator:v1.0.1 + image: randao/orchestrator:v1.0.2 depends_on: postgres: condition: service_healthy @@ -36,6 +41,11 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + logging: + driver: json-file + options: + max-size: "100m" + max-file: "5" networks: backend: diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 131d15f..0ca9e3f 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.1 # You can change this value to any version you want +export VERSION=v1.0.2 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index fb85e27..3e1ebf4 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -18,7 +18,7 @@ }, "name": "ao-vrf", "description": "1. To build:\r ```\r docker build -t serverless-multi-cloud .\r ```", - "version": "1.0.0", + "version": "1.0.2", "main": "Organizer.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 679626b..584b20e 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,8 +1,7 @@ import Docker from 'dockerode'; import { connectWithRetry, setupDatabase } from './db_tools.js'; import Arweave from 'arweave'; -import { checkAndFetchIfNeeded, cleanupFulfilledEntries, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown, initializeAutoUpdateTimer } from './helperFunctions.js'; -import { monitorDockerContainers } from './containerManagment.js'; +import { checkAndFetchIfNeeded, cleanupFulfilledEntries, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown } from './helperFunctions.js'; import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; @@ -133,48 +132,18 @@ async function polling(client: any) { // Main function async function run(): Promise { - // Initialize logger from environment variables logger.info("Orchestrator starting up"); logger.debug("Environment variables loaded, initializing services"); - + const client = await connectWithRetry(); await setupDatabase(client); - - // Get database entry count on startup - try { - const res = await client.query('SELECT COUNT(*) as count FROM time_lock_puzzles WHERE request_id IS NULL'); - const entriesAvailable = parseInt(res.rows[0].count, 10); - logger.info(`Initial database entry count: ${entriesAvailable}`); - } catch (error) { - logger.error("Error getting initial database count:", error); - } arweave.wallets.jwkToAddress(JSON.parse(process.env.WALLET_JSON!)).then((address) => { logger.info(`Provider ID: ${address}`); PROVIDER_ID = address; - - // Initialize auto-update timer for provider values - initializeAutoUpdateTimer(); }); - // setInterval(async () => { - // const res = await client.query('SELECT COUNT(*) as count FROM time_lock_puzzles'); - // logger.debug(`Periodic log - Current database size: ${res.rows[0].count}`); - // // Check and fetch entries for the database if needed - // logger.debug("Step 0: Checking and fetching database entries if below threshold."); - // checkAndFetchIfNeeded(client, PROVIDER_ID).catch((error) => { - // logger.error("Error in checkAndFetchIfNeeded:", error); - // }); - // }, DATABASE_CHECK_TIME); - - setInterval(async () => { - await monitorDockerContainers(); - }, DOCKER_MONITORING_TIME); // Cleanup every 30 seconds - - setInterval(async () => { - await polling(client); - }, POLLING_INTERVAL_MS); - + // Handle graceful shutdown before entering infinite loop process.on("SIGTERM", async () => { logger.info("SIGTERM received. Shutting down gracefully."); await client.end(); @@ -182,6 +151,20 @@ async function run(): Promise { await Logger.close(); // Use the static close method on the Logger class process.exit(0); }); +//TODO SEE WHATS BETTER (This could possibly have a new tx queed up while the old one is in the works to keep it speeds but who knows) + // setInterval(async () => { + // await polling(client); + // }, POLLING_INTERVAL_MS); + + // Infinite polling loop + while (true) { + try { + await polling(client); + } catch (error) { + logger.error("Polling error:", error); + } + } } + run().catch((err) => logger.error(`Error in main function: ${err}`)); diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts index d866d56..b1d48bb 100644 --- a/orchestrator/src/containerManagment.ts +++ b/orchestrator/src/containerManagment.ts @@ -81,14 +81,8 @@ export async function triggerTimePuzzleJobPod(randomCount: number): Promise 0) { logger.info("A puzzle-gen container is already running. Skipping new container launch."); return null; @@ -115,7 +109,10 @@ import { resetOngoingRandomGeneration } from './helperFunctions.js'; // Function to wait for Docker containers to complete and remove them from tracking export async function monitorDockerContainers(): Promise { - if (ongoingContainers.size === 0) return; + if (ongoingContainers.size === 0){ + resetOngoingRandomGeneration(); + return; + } logger.verbose(`Monitoring ${ongoingContainers.size} Docker containers`); let containersRemoved = false; diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 6b802bf..f72c66f 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -1,7 +1,7 @@ import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RequestList } from "ao-process-clients"; import { Client } from "pg"; import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; -import { getMoreRandom } from "./containerManagment"; +import { getMoreRandom, monitorDockerContainers } from "./containerManagment"; import logger, { LogLevel } from "./logger"; import { monitoring } from "./monitoring"; import { setTimeout, setInterval } from 'timers'; @@ -12,16 +12,15 @@ const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds let current_onchain_random = - 10 // Cooldown tracking for updateAvailableValuesAsync -let lastUpdateTimestamp = 0; -let isUpdateOnCooldown = false; -let lastAutoUpdateTimestamp = 0; +let lastUpdatedOnChainTime = 0; +const FIFTEEN_MINUTES_MS = 15 * 60 * 1000; // Cooldown tracking for fulfillRandomChallenge and fulfillRandomOutput (per request ID) const challengeCooldowns = new Map(); const outputCooldowns = new Map(); // Track whether there's an ongoing random generation request let ongoingRandomGeneration = false; -const MAX_RANDOM_PER_REQUEST = 100; // Maximum number of random values to generate in a single request +const MAX_RANDOM_PER_REQUEST = 500; // Maximum number of random values to generate in a single request // Function to reset the ongoingRandomGeneration flag export function resetOngoingRandomGeneration() { @@ -36,20 +35,21 @@ setInterval(() => { export async function getRandomClient(): Promise { const currentTime = Date.now(); - + if (!randomClientInstance || (currentTime - lastInitTime) > REINIT_INTERVAL) { logger.debug("Initializing RandomClient"); randomClientInstance = ((await RandomClient.defaultBuilder())) .withWallet(JSON.parse(process.env.WALLET_JSON!)) .withAOConfig({ CU_URL: "https://ur-cu.randao.net", + MU_URL: "https://ur-mu.randao.net", MODE: "legacy" }) .build(); lastInitTime = currentTime; logger.debug("RandomClient initialized"); } - + return randomClientInstance; } @@ -71,9 +71,9 @@ export async function processChallengeRequests( try { await client.query('BEGIN'); // Start transaction - + logger.debug(`${parentLogId} Fetching existing request mappings.`); - + // Fetch already assigned request_id -> dbId mappings const existingMappingsRes = await client.query( `SELECT request_id FROM time_lock_puzzles @@ -81,16 +81,16 @@ export async function processChallengeRequests( FOR UPDATE SKIP LOCKED`, [requestIds] ); - + const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); logger.debug(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); - + // Find only the unmapped requests (requestIds not in existingRequestIds) const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); logger.debug(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); - + let mappedEntries: { requestId: string, dbId: number }[] = []; - + if (unmappedRequestIds.length > 0) { logger.debug(`${parentLogId} Fetching available DB entries.`); const dbRes = await client.query( @@ -101,13 +101,13 @@ export async function processChallengeRequests( FOR UPDATE SKIP LOCKED`, [unmappedRequestIds.length] ); - + const availableDbEntries = dbRes.rows.map(row => row.id); logger.debug(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); - + if (availableDbEntries.length > 0) { const numMappings = Math.min(unmappedRequestIds.length, availableDbEntries.length); - + for (let i = 0; i < numMappings; i++) { await client.query( `UPDATE time_lock_puzzles @@ -122,34 +122,34 @@ export async function processChallengeRequests( logger.warn(`${parentLogId} No available DB entries for unmapped requests.`); } } - + // Collect all request IDs (previously mapped + newly mapped) const allRequestIds = [...existingRequestIds, ...mappedEntries.map(entry => entry.requestId)]; - + if (allRequestIds.length === 0) { logger.info(`${parentLogId} No requests to process. Committing transaction.`); await client.query('COMMIT'); return; } - + await client.query('COMMIT'); // Commit all updates at once logger.info(`${parentLogId} Committed all changes. Now fulfilling challenges.`); - + // Call fulfillRandomChallenge for all request IDs await Promise.all( - allRequestIds.map(requestId => + allRequestIds.map(requestId => fulfillRandomChallenge(client, requestId, parentLogId) .catch(error => logger.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) ) ); - + logger.info(`${parentLogId} All challenges fulfilled`); - } catch (error:any) { + } catch (error: any) { logger.error(`${parentLogId} Error in processChallengeRequests:`, error); await client.query('ROLLBACK'); // Rollback on failure - + logger.error(`SQL State: ${error.code}, Message: ${error.message}`); - } + } } // Step 3: Process Output Requests @@ -237,12 +237,12 @@ export async function cleanupFulfilledEntries( SELECT rsa_id FROM time_lock_puzzles WHERE id = ANY($1) ); `, [markForDeletion]); - + await client.query(` DELETE FROM time_lock_puzzles WHERE id = ANY($1); `, [markForDeletion]); - + logger.info(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); } @@ -315,12 +315,6 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri // Function to check and fetch database entries as needed export async function checkAndFetchIfNeeded(client: Client) { - // First check if a random generation is already in progress - if (ongoingRandomGeneration) { - logger.info('A random generation process is already running. Skipping new request.'); - return; - } - try { // Query current count of usable DB entries const res = await client.query( @@ -334,21 +328,24 @@ export async function checkAndFetchIfNeeded(client: Client) { logger.warn("Value is -1"); logger.warn("Provider has been shut down by USER..."); logger.warn("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-1); break; case -2: logger.error("Value is -2"); logger.error("Provider has been shut down by PROCESS..."); logger.error("This is due to One of the following: "); logger.error("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); - logger.error("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)" ); + logger.error("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)"); logger.error("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); logger.error("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-2); break; case -3: logger.warn("Value is -3"); logger.warn("Provider has been shut down by PROCESS..."); logger.warn("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); logger.warn("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-3); break; case -10: logger.info("Value is -10"); @@ -359,92 +356,57 @@ export async function checkAndFetchIfNeeded(client: Client) { logger.debug("Provider is up and working"); logger.debug(`Onchain Value is ${current_onchain_random}`); logger.debug(`Local Value is ${currentCount}`); - if (Math.abs(current_onchain_random - currentCount) > UNCHAIN_VS_OFFCHAIN_MAX_DIF) { - logger.info(`Updating available random values from ${current_onchain_random} to ${currentCount}`); - updateAvailableValuesAsync(currentCount); - } else { - logger.debug(`Not updating onchain values to avoid unneeded onchain messages. When difference is over ${UNCHAIN_VS_OFFCHAIN_MAX_DIF} an update will happen`); - } + await updateAvailableValuesAsync(currentCount); + + } + + // First check if a random generation is already in progress. If so, see if it's done + if (ongoingRandomGeneration) { + logger.info('A random generation process is already running. Skipping new request.'); + await monitorDockerContainers(); + return; } // Check if more entries are needed if (currentCount >= MINIMUM_ENTRIES) return; - + // Set the flag to prevent concurrent random generation ongoingRandomGeneration = true; - + // Calculate how many random values we need const entriesNeeded = MINIMUM_ENTRIES - currentCount; // Limit to MAX_RANDOM_PER_REQUEST const randomToGenerate = Math.min(entriesNeeded, MAX_RANDOM_PER_REQUEST); - + logger.info(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${randomToGenerate} entries (out of ${entriesNeeded} needed)...`); - + // Start the random generation with the calculated amount - await getMoreRandom(currentCount, randomToGenerate); + await getMoreRandom(randomToGenerate); } catch (error) { logger.error('Error during check and fetch:', error); ongoingRandomGeneration = false; // Reset the flag on error } - // Note: we don't reset ongoingRandomGeneration here - it will be reset by the container monitoring } +export async function updateAvailableValuesAsync(currentCount: number) { + const now = Date.now(); -export function updateAvailableValuesAsync(currentCount: number, forceUseOnchainValue: boolean = false) { - // If on cooldown, log and exit without updating - if (isUpdateOnCooldown) { - logger.debug(`Update skipped - on cooldown (${Math.floor((30000 - (Date.now() - lastUpdateTimestamp)) / 1000)}s remaining)`); + if (now - lastUpdatedOnChainTime < FIFTEEN_MINUTES_MS) { + logger.info(`On-chain update skipped - only ${Math.floor((now - lastUpdatedOnChainTime) / 1000)}s since last update`); return; } - return (async () => { - try { - // Set cooldown status - isUpdateOnCooldown = true; - lastUpdateTimestamp = Date.now(); - - // Determine which value to use - const valueToUpdate = forceUseOnchainValue ? current_onchain_random : currentCount; - - // Get monitoring data including system specs and performance metrics - const monitoringData = await monitoring.getMonitoringData(); - - // Send the count and monitoring data to the AO process - await (await getRandomClient()).updateProviderAvailableValues(valueToUpdate, monitoringData); - logger.info(`Updated provider values to ${valueToUpdate}${forceUseOnchainValue ? ' (using on-chain value)' : ''}`); - - // Track last auto-update time if this was a forced update - if (forceUseOnchainValue) { - lastAutoUpdateTimestamp = Date.now(); - } - - // Set timeout to release cooldown after 30 seconds - setTimeout(() => { - isUpdateOnCooldown = false; - logger.debug('Update cooldown period ended'); - }, 30000); - } catch (error) { - logger.error("Failed to update provider values:", error); - monitoring.incrementErrorCount(); - // Release cooldown on error after 5 seconds to allow retry - setTimeout(() => { - isUpdateOnCooldown = false; - logger.debug('Update cooldown period ended (after error)'); - }, 5000); - } - })(); -} - -export async function getProviderAvailableRandomValues(PROVIDER_ID: string): Promise { try { - return { - providerId: PROVIDER_ID, - availibleRandomValues: current_onchain_random - }; + const monitoringData = await monitoring.getMonitoringData(); + + await (await getRandomClient()).updateProviderAvailableValues(currentCount, monitoringData); + logger.info(`Updated provider values to ${currentCount}`); + + lastUpdatedOnChainTime = now; } catch (error) { - logger.error(`Error fetching available random values:`, error); - return {} as GetProviderAvailableValuesResponse; + logger.error("Failed to update provider values:", error); + monitoring.incrementErrorCount(); } } @@ -455,11 +417,11 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL logger.debug(`${parentLogId} Skipping challenge for request ID ${requestId} - on cooldown`); return; } - + try { // Mark this request ID as being processed to prevent duplicates challengeCooldowns.set(requestId, true); - + // Fetch the necessary details from the database using requestId const res = await client.query( `SELECT id, modulus, x @@ -486,7 +448,7 @@ async function fulfillRandomChallenge(client: Client, requestId: string, parentL } }); logger.info(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); - + // Set a timeout to release the cooldown after 1 second setTimeout(() => { challengeCooldowns.delete(requestId); @@ -506,11 +468,11 @@ async function fulfillRandomOutput(client: Client, requestId: string, parentLogI logger.debug(`${parentLogId} Skipping output for request ID ${requestId} - on cooldown`); return; } - + try { // Mark this request ID as being processed to prevent duplicates outputCooldowns.set(requestId, true); - + // Fetch the output and proof from the database using the requestId const res = await client.query( `SELECT @@ -520,23 +482,23 @@ async function fulfillRandomOutput(client: Client, requestId: string, parentLogI rk.q FROM time_lock_puzzles tlp JOIN rsa_keys rk ON tlp.rsa_id = rk.id - WHERE tlp.request_id = $1`, + WHERE tlp.request_id = $1`, [requestId] ); - + if (!res.rowCount) { logger.error(`No entry found for request ID: ${requestId}`); return; } - + // Map the response to structured variables - const { - id: dbId, + const { + id: dbId, output, // Mapping 'y' to 'output' - p: rsaP, - q: rsaQ + p: rsaP, + q: rsaQ } = res.rows[0]; - + logger.debug(`${parentLogId} Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}, output: ${output}, rsaP: ${rsaP}, rsaQ ${rsaQ} `); logger.info(`${parentLogId} Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); @@ -548,7 +510,7 @@ async function fulfillRandomOutput(client: Client, requestId: string, parentLogI } }); logger.info(`${parentLogId} Proof posted for request ID: ${requestId}`); - + // Set a timeout to release the cooldown after 1 second setTimeout(() => { outputCooldowns.delete(requestId); @@ -561,32 +523,14 @@ async function fulfillRandomOutput(client: Client, requestId: string, parentLogI } } -// Initialize automatic update timer -export function initializeAutoUpdateTimer() { - logger.info('Starting auto update timer for provider values'); - lastAutoUpdateTimestamp = Date.now(); // Initialize timer start - - // Set up interval check that runs every minute - setInterval(() => { - const timeSinceLastUpdate = Date.now() - lastAutoUpdateTimestamp; - const tenMinutesInMs = 10 * 60 * 1000; - - if (timeSinceLastUpdate >= tenMinutesInMs) { - logger.info(`Auto-update triggered - ${Math.floor(timeSinceLastUpdate / 60000)} minutes since last update`); - updateAvailableValuesAsync(0, true); // Force use of on-chain value - } - }, 60000); // Check every minute -} - export async function shutdown() { + //TODO do post 0 avalible random then do like as much polling as you can before turning off to ensure all random gets pushed through try { - const randomClient = await getRandomClient(); - - // Get monitoring data for final update - const monitoringData = await monitoring.getMonitoringData(); - + // // Get monitoring data for final update + // const monitoringData = await monitoring.getMonitoringData(); + // Set provider available values to 0 and include final monitoring data - const message = await randomClient.updateProviderAvailableValues(0, monitoringData); + const message = await (await getRandomClient()).updateProviderAvailableValues(0); logger.info(String(message)); // Convert message to string logger.info(`Updated provider values to 0`); } catch (error) { From 25d8ed00e9424b025db57995b7ea57265e3e6430 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 22 May 2025 13:56:20 -0400 Subject: [PATCH 60/80] 1.0.3 --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 4 +- orchestrator/src/app.ts | 13 +- orchestrator/src/helperFunctions.ts | 9 +- orchestrator/src/monitoring.ts | 201 ++++++++++++++++------------ 6 files changed, 137 insertions(+), 94 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index d930bd6..5cf7ad5 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -23,7 +23,7 @@ services: max-file: "5" orchestrator: - image: randao/orchestrator:v1.0.2 + image: randao/orchestrator:v1.0.3 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 0ca9e3f..b2d11c1 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.2 # You can change this value to any version you want +export VERSION=v1.0.3 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 3e1ebf4..9eafe5e 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^6.0.58", + "ao-process-clients": "^6.0.59", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", @@ -18,7 +18,7 @@ }, "name": "ao-vrf", "description": "1. To build:\r ```\r docker build -t serverless-multi-cloud .\r ```", - "version": "1.0.2", + "version": "1.0.3", "main": "Organizer.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 584b20e..c8201ce 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -9,7 +9,7 @@ export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; -export const DOCKER_MONITORING_TIME= 30000; +export const DOCKER_MONITORING_TIME = 30000; export const POLLING_INTERVAL_MS = 0; //0 second export const DATABASE_CHECK_TIME = 60000; //60 seconds export const MINIMUM_ENTRIES = 5000; @@ -18,6 +18,8 @@ export const MAX_RETRIES = 10; export const RETRY_DELAY_MS = 10000; export const COMPLETION_RETENTION_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds export const UNCHAIN_VS_OFFCHAIN_MAX_DIF = 50; +export const MINIMUM_RANDOM_DELTA = 25; +export const SHUTDOWN_POLLING_DELAY = 10; let PROVIDER_ID = ""; let pollingInProgress = false; @@ -148,10 +150,17 @@ async function run(): Promise { logger.info("SIGTERM received. Shutting down gracefully."); await client.end(); await shutdown(); + for (let i = 0; i < SHUTDOWN_POLLING_DELAY; i++) { + try { + await polling(client); + } catch (error) { + logger.error(`Shutdown Polling iteration ${i + 1} failed:`, error); + } + } await Logger.close(); // Use the static close method on the Logger class process.exit(0); }); -//TODO SEE WHATS BETTER (This could possibly have a new tx queed up while the old one is in the works to keep it speeds but who knows) + //TODO SEE WHATS BETTER (This could possibly have a new tx queed up while the old one is in the works to keep it speeds but who knows) // setInterval(async () => { // await polling(client); // }, POLLING_INTERVAL_MS); diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index f72c66f..a48894d 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -1,6 +1,6 @@ import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RequestList } from "ao-process-clients"; import { Client } from "pg"; -import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; +import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, MINIMUM_RANDOM_DELTA, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; import { getMoreRandom, monitorDockerContainers } from "./containerManagment"; import logger, { LogLevel } from "./logger"; import { monitoring } from "./monitoring"; @@ -368,13 +368,12 @@ export async function checkAndFetchIfNeeded(client: Client) { } // Check if more entries are needed - if (currentCount >= MINIMUM_ENTRIES) return; + const entriesNeeded = MINIMUM_ENTRIES - currentCount; + if (entriesNeeded < MINIMUM_RANDOM_DELTA) return; // Set the flag to prevent concurrent random generation ongoingRandomGeneration = true; - // Calculate how many random values we need - const entriesNeeded = MINIMUM_ENTRIES - currentCount; // Limit to MAX_RANDOM_PER_REQUEST const randomToGenerate = Math.min(entriesNeeded, MAX_RANDOM_PER_REQUEST); @@ -532,7 +531,7 @@ export async function shutdown() { // Set provider available values to 0 and include final monitoring data const message = await (await getRandomClient()).updateProviderAvailableValues(0); logger.info(String(message)); // Convert message to string - logger.info(`Updated provider values to 0`); + logger.info(`Updated provider values to 0... shutting down ...`); } catch (error) { logger.error("Failed to update provider values:", error); monitoring.incrementErrorCount(); diff --git a/orchestrator/src/monitoring.ts b/orchestrator/src/monitoring.ts index f65fe07..c9545df 100644 --- a/orchestrator/src/monitoring.ts +++ b/orchestrator/src/monitoring.ts @@ -5,6 +5,7 @@ import { promisify } from 'util'; import crypto from 'crypto'; import { version } from '../package.json'; import logger from './logger'; +import { MonitoringData, PerformanceMetrics, SystemSpecs, ExecutionMetrics, HealthStatus } from 'ao-process-clients'; const execAsync = promisify(exec); @@ -17,19 +18,11 @@ interface StepTimings { overall: number; } -// Performance counters -interface NetworkCounters { - rxBytes: number; - txBytes: number; - rxPackets: number; - txPackets: number; -} - // Class to manage all monitoring data export class MonitoringService { private static instance: MonitoringService; private machineId: string; - + // Metrics tracking private stepTimings: StepTimings = { step1: 0, @@ -38,7 +31,7 @@ export class MonitoringService { step4: 0, overall: 0 }; - + private totalStepSamples: { [key: string]: number } = { step1: 0, step2: 0, @@ -46,136 +39,166 @@ export class MonitoringService { step4: 0, overall: 0 }; - + private errorCount: number = 0; - private previousNetworkStats: NetworkCounters | null = null; + private errorTimestamps: number[] = []; // store Unix timestamps + // Network monitoring properties + private static previousNetworkBytes = { rx: 0, tx: 0 }; + private static lastCheckTime = Date.now(); + private constructor() { this.machineId = this.generateMachineId(); - + // Initialize network stats - this.updateNetworkStats().catch(err => + this.updateNetworkStats().catch(err => logger.error('Failed to initialize network stats:', err) ); } - + public static getInstance(): MonitoringService { if (!MonitoringService.instance) { MonitoringService.instance = new MonitoringService(); } return MonitoringService.instance; } - + private generateMachineId(): string { try { // Using stable hardware identifiers that won't change between restarts // but will be unique to physical/virtual machines - + // Get CPU information which is generally the same across containers on same host const cpuModel = os.cpus()[0]?.model || ''; const cpuSpeed = os.cpus()[0]?.speed || 0; const totalCores = os.cpus().length; - // System memory size is usually fixed for a machine const totalMemory = os.totalmem(); - + // Combine all available identifiers with more hardware specs const hwInfo = `${cpuModel}-${cpuSpeed}-${totalCores}-${totalMemory}-${os.platform()}-${os.arch()}`; - + // Generate a shorter hash (first 16 chars of SHA-256) for easier identification while maintaining uniqueness return crypto.createHash('sha256').update(hwInfo).digest('hex').substring(0, 16); } catch (error) { logger.error('Error generating machine ID:', error); - // Fallback to a less reliable but still somewhat useful ID - return crypto.createHash('sha256').update(os.hostname() + os.platform()).digest('hex').substring(0, 16); + // Fallback to a null entry for those who care + return '0000000000000000'; } } - - private async updateNetworkStats(): Promise { + + private async updateNetworkStats(): Promise<{ rx_sec: number; tx_sec: number }> { try { - let networkStats: NetworkCounters = { - rxBytes: 0, - txBytes: 0, - rxPackets: 0, - txPackets: 0 + // Store current and previous bytes for rate calculation + const currentBytes = { + rx: 0, + tx: 0 + }; + + // Initialize network stats with default values + const networkStats: { rx_sec: number; tx_sec: number } = { + rx_sec: 0, + tx_sec: 0 }; if (process.platform === 'linux') { // Linux - read from /proc/net/dev const netDev = await fs.promises.readFile('/proc/net/dev', 'utf8'); - const interfaces = netDev.split('\n').filter(line => + const interfaces = netDev.split('\n').filter(line => line.includes(':') && !line.includes('lo:') ); - + for (const intf of interfaces) { const parts = intf.trim().split(/\s+/); - networkStats.rxBytes += parseInt(parts[1] || '0', 10); - networkStats.rxPackets += parseInt(parts[2] || '0', 10); - networkStats.txBytes += parseInt(parts[9] || '0', 10); - networkStats.txPackets += parseInt(parts[10] || '0', 10); + currentBytes.rx += parseInt(parts[1] || '0', 10); + currentBytes.tx += parseInt(parts[9] || '0', 10); } } else if (process.platform === 'win32') { // Windows - use PowerShell to get network stats const { stdout } = await execAsync( - 'powershell "Get-NetAdapterStatistics | Select-Object ReceivedBytes,ReceivedPackets,SentBytes,SentPackets | ConvertTo-Json"' + 'powershell "Get-NetAdapterStatistics | Select-Object ReceivedBytes,SentBytes | ConvertTo-Json"' ); - + try { const stats = JSON.parse(stdout); const adapters = Array.isArray(stats) ? stats : [stats]; for (const adapter of adapters) { - networkStats.rxBytes += adapter.ReceivedBytes || 0; - networkStats.rxPackets += adapter.ReceivedPackets || 0; - networkStats.txBytes += adapter.SentBytes || 0; - networkStats.txPackets += adapter.SentPackets || 0; + currentBytes.rx += adapter.ReceivedBytes || 0; + currentBytes.tx += adapter.SentBytes || 0; } } catch (e) { logger.error('Failed to parse network stats:', e); } } - this.previousNetworkStats = networkStats; + // Calculate rates in bytes per second + const now = Date.now(); + const timeDiffSeconds = (now - MonitoringService.lastCheckTime) / 1000; + + if (timeDiffSeconds > 0 && MonitoringService.previousNetworkBytes.rx > 0) { + // Calculate rates only if we have previous measurements + networkStats.rx_sec = Math.max(0, (currentBytes.rx - MonitoringService.previousNetworkBytes.rx) / timeDiffSeconds); + networkStats.tx_sec = Math.max(0, (currentBytes.tx - MonitoringService.previousNetworkBytes.tx) / timeDiffSeconds); + } + + // Store current values for next calculation + MonitoringService.previousNetworkBytes = { ...currentBytes }; + MonitoringService.lastCheckTime = now; + return networkStats; } catch (error) { logger.error('Error getting network stats:', error); return { - rxBytes: 0, - txBytes: 0, - rxPackets: 0, - txPackets: 0 + rx_sec: 0, + tx_sec: 0 }; } } - + public updateStepTiming(step: string, timeTaken: number): void { if (step in this.stepTimings) { // Calculate running average const currentSamples = this.totalStepSamples[step]; const currentAvg = this.stepTimings[step as keyof StepTimings]; - + // Update running average - this.stepTimings[step as keyof StepTimings] = + this.stepTimings[step as keyof StepTimings] = (currentAvg * currentSamples + timeTaken) / (currentSamples + 1); this.totalStepSamples[step]++; } } - + public incrementErrorCount(): void { this.errorCount++; + const now = Date.now(); + this.errorTimestamps.push(now); + } + + private countErrorsSince(msAgo: number): number { + const cutoff = Date.now() - msAgo; + return this.errorTimestamps.filter(ts => ts >= cutoff).length; } - public async getMonitoringData(): Promise { + private cleanupOldErrors(): void { + const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; + this.errorTimestamps = this.errorTimestamps.filter(ts => ts >= oneDayAgo); + } + + public async getMonitoringData(): Promise { // Get real-time system metrics const cpuInfo = os.cpus(); const loadAvg = os.loadavg(); const totalMemory = os.totalmem(); const freeMemory = os.freemem(); const usedMemoryPercent = Math.round((1 - freeMemory / totalMemory) * 100); - + // Get disk info - only used percent let diskUsedPercent = 0; - + try { + // Cleanup old errors to reduce memory + this.cleanupOldErrors(); + if (process.platform === 'linux') { const { stdout } = await execAsync('df -h / --output=pcent'); const lines = stdout.trim().split('\n'); @@ -186,7 +209,7 @@ export class MonitoringService { const { stdout } = await execAsync( 'powershell "Get-Volume | Where-Object {$_.DriveLetter -eq \'C\'} | Select-Object @{Name=\'UsedPercent\';Expression={100 - (($_.SizeRemaining / $_.Size) * 100)}} | ConvertTo-Json"' ); - + try { const diskData = JSON.parse(stdout); diskUsedPercent = Math.round(diskData.UsedPercent || 0); @@ -197,39 +220,51 @@ export class MonitoringService { } catch (error) { logger.error('Error getting disk info:', error); } - + // Get updated network stats const networkStats = await this.updateNetworkStats(); - - // Construct monitoring data object - const monitoringData = { + + // Create the SystemSpecs object + const systemSpecs: SystemSpecs = { + arch: os.arch(), + uptime: os.uptime(), + cpuCount: cpuInfo.length, + memoryTotalBytes: totalMemory, + token: this.machineId + }; + + // Create the PerformanceMetrics object + const performance: PerformanceMetrics = { + loadAverage: loadAvg, + memoryUsedPercent: usedMemoryPercent, + diskUsedPercent: diskUsedPercent, + network: networkStats + }; + + // Create the ExecutionMetrics object + const executionMetrics: ExecutionMetrics = { + stepTimingsMs: this.stepTimings as unknown as Record + }; + + // Create the HealthStatus object + const health: HealthStatus = { + errorTotal: this.errorCount, + errorsLastHour: this.countErrorsSince(60 * 60 * 1000), + errorsLastDay: this.countErrorsSince(24 * 60 * 60 * 1000), + status: this.countErrorsSince(60 * 60 * 1000) > 10 ? "degraded" : "healthy" + }; + + // Construct the full MonitoringData object + const monitoringData: MonitoringData = { providerVersion: version, - - systemSpecs: { - arch: os.arch(), - cpuCount: cpuInfo.length, - memoryTotalBytes: totalMemory, - token: this.machineId - }, - - performance: { - loadAverage: loadAvg, - memoryUsedPercent: usedMemoryPercent, - diskUsedPercent: diskUsedPercent, - network: networkStats - }, - - executionMetrics: { - stepTimingsMs: this.stepTimings - }, - - health: { - errors: this.errorCount, - status: this.errorCount > 10 ? "degraded" : "healthy" - } + timestamp: new Date().toISOString(), + systemSpecs, + performance, + executionMetrics, + health }; - - return JSON.stringify(monitoringData); + + return monitoringData; } } From 9a9377856443128849c07e2066c9004a5f1b0eff Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 29 May 2025 11:08:28 -0400 Subject: [PATCH 61/80] one --- orchestrator/src/app.ts | 5 ++++- orchestrator/src/helperFunctions.ts | 10 ++++++++++ requester/package.json | 2 +- requester/src/app.ts | 4 +++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index c8201ce..3ee9e58 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,7 +1,7 @@ import Docker from 'dockerode'; import { connectWithRetry, setupDatabase } from './db_tools.js'; import Arweave from 'arweave'; -import { checkAndFetchIfNeeded, cleanupFulfilledEntries, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown } from './helperFunctions.js'; +import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown } from './helperFunctions.js'; import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; @@ -110,6 +110,9 @@ async function polling(client: any) { //TODO enable this again later await cleanupFulfilledEntries(client, openRequests, logId); await checkAndFetchIfNeeded(client) + + crank(); //TODO find a better place for this + const timeTaken = Date.now() - s4; stepTracking.step4 = { completed: true, timeTaken }; // Update monitoring with step timing diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index a48894d..c82ee0e 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -255,6 +255,16 @@ export async function cleanupFulfilledEntries( logger.info(`${parentLogId} Step 4 completed.`); } + +//TODO fill in logic for seeinging cranking needs to be done (Also upgrade this to be less message heavy) +export async function crank() { + // 1% chance to run the crank + if (Math.random() < 0.01) { + logger.info(`Cranking!!!`); + await (await getRandomClient()).crank(); + } +} + export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { const defaultResponse: GetOpenRandomRequestsResponse = { providerId: PROVIDER_ID, diff --git a/requester/package.json b/requester/package.json index 94e7566..14fb257 100644 --- a/requester/package.json +++ b/requester/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "@permaweb/aoconnect": "^0.0.78", - "ao-process-clients": "^6.0.58", + "ao-process-clients": "^6.0.63", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index 453f415..566f1d8 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -124,6 +124,7 @@ async function main() { const randclient = await getRandomClient() //const stakeclient = ProviderStakingClient.autoConfiguration(); + randclient.prepay(1000000000) while (true) { console.log("Running") try { @@ -133,7 +134,8 @@ async function main() { const callbackId = `callback-${Date.now()}`; const { providers, count } = await getRandomProviders(randclient); console.log(`Selected ${count} providers:`, providers); - await randclient.createRequest(providers, count, callbackId); + // await randclient.createRequest(providers, count, callbackId); + randclient.redeem(providers, count, callbackId); //await TransferToProviders(providers, callbackId) //await randclient.createRequest(["X1tqliRkKnClhVQ4aIeyuOaPTzr5PfnxqAoSdpTzZy8"], 1, "123"); totalRandomCalled++; From fddb8e5fe1e161f705ac8ad0a08c05213dc84c4c Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 31 May 2025 11:50:39 -0400 Subject: [PATCH 62/80] v1.0.4 --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 4 - orchestrator/src/app.ts | 2 + orchestrator/src/helperFunctions.ts | 137 +++++++++++++++++++++++++--- orchestrator/src/monitoring.ts | 4 +- requester/package.json | 2 +- requester/src/app.ts | 6 +- 8 files changed, 136 insertions(+), 23 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 5cf7ad5..c600ff2 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -23,7 +23,7 @@ services: max-file: "5" orchestrator: - image: randao/orchestrator:v1.0.3 + image: randao/orchestrator:v1.0.4 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index b2d11c1..b7393aa 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.3 # You can change this value to any version you want +export VERSION=v1.0.4 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 9eafe5e..20cacb0 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -16,10 +16,6 @@ "dockerode": "^4.0.2", "pg": "^8.13.1" }, - "name": "ao-vrf", - "description": "1. To build:\r ```\r docker build -t serverless-multi-cloud .\r ```", - "version": "1.0.3", - "main": "Organizer.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 3ee9e58..d2ef5d2 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -5,6 +5,8 @@ import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderReque import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; +export const VERSION = process.env.VERSION || "test"; + export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index c82ee0e..274a870 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -22,6 +22,9 @@ const outputCooldowns = new Map(); let ongoingRandomGeneration = false; const MAX_RANDOM_PER_REQUEST = 500; // Maximum number of random values to generate in a single request +// Map to track request timestamps +const requestTimestamps: Map = new Map(); + // Function to reset the ongoingRandomGeneration flag export function resetOngoingRandomGeneration() { ongoingRandomGeneration = false; @@ -256,14 +259,59 @@ export async function cleanupFulfilledEntries( } -//TODO fill in logic for seeinging cranking needs to be done (Also upgrade this to be less message heavy) +/** + * Function to log request timestamps + * Adds new request IDs to the tracking map and removes ones that are no longer present + * @param allRequestIds Array of request IDs to track + */ +export function logRequestTimestamps(allRequestIds: string[]): void { + const currentTime = Date.now(); + const existingIds = new Set(requestTimestamps.keys()); + + // Add new request IDs with current timestamp + for (const requestId of allRequestIds) { + if (!requestTimestamps.has(requestId)) { + logger.verbose(`Adding new request ID to tracking: ${requestId}`); + requestTimestamps.set(requestId, currentTime); + } + } + + // Remove request IDs that are no longer present + for (const existingId of existingIds) { + if (!allRequestIds.includes(existingId)) { + logger.verbose(`Removing request ID from tracking: ${existingId}`); + requestTimestamps.delete(existingId); + } + } +} + +// Function to check for defunct requests and crank if needed export async function crank() { - // 1% chance to run the crank - if (Math.random() < 0.01) { - logger.info(`Cranking!!!`); - await (await getRandomClient()).crank(); + const currentTime = Date.now(); + const defunctRequestIds: string[] = []; + const DEFUNCT_THRESHOLD_MS = 30 * 1000; // 30 seconds + + // Check for defunct request IDs (those that have been in the map for over 30 seconds) + requestTimestamps.forEach((timestamp, requestId) => { + const timeInMap = currentTime - timestamp; + if (timeInMap > DEFUNCT_THRESHOLD_MS) { + defunctRequestIds.push(requestId); + logger.info(`Defunct request found: ${requestId} (in system for ${Math.floor(timeInMap / 1000)} seconds)`); + } + }); + + // If there are any defunct requests, run the crank +if (defunctRequestIds.length > 0) { + logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); + (await getRandomClient()).crank(); +} else { + // 1 in 100 chance to crank + if (Math.floor(Math.random() * 100) === 0) { + logger.info("Cranking randomly (1 in 100 chance hit)"); + (await getRandomClient()).crank(); } } +} export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { const defaultResponse: GetOpenRandomRequestsResponse = { @@ -273,6 +321,61 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri }; try { const response = await (await getRandomClient()).getAllProviderActivity(); + + // Collect all request IDs from all providers for tracking + const allRequestIds: string[] = []; + + // Process each provider to extract request IDs + for (const provider of response) { + try { + // Extract challenge request IDs + if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { + try { + const parsedChallengeData = JSON.parse(provider.active_challenge_requests); + if (parsedChallengeData && typeof parsedChallengeData === 'object' && 'request_ids' in parsedChallengeData) { + const requestIds = parsedChallengeData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + logger.warn(`${parentLogId} Warning: Failed to parse challenge requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + + // Extract output request IDs + if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { + try { + const parsedOutputData = JSON.parse(provider.active_output_requests); + if (parsedOutputData && typeof parsedOutputData === 'object' && 'request_ids' in parsedOutputData) { + const requestIds = parsedOutputData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + logger.warn(`${parentLogId} Warning: Failed to parse output requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + } catch (err) { + logger.warn(`${parentLogId} Warning: Failed to process provider ${provider.provider_id}:`, err); + } + } + + // Log all the request IDs for tracking + logger.debug(`${parentLogId} Found ${allRequestIds.length} request IDs across all providers`); + + // Update the request timestamps tracking + logRequestTimestamps(allRequestIds); + const provider = response.find(p => p.provider_id === PROVIDER_ID); if (!provider) { @@ -285,18 +388,30 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri let parsedOutputRequests: RequestList = { request_ids: [] }; try { - if (provider.active_challenge_requests) { - //@ts-ignore - parsedChallengeRequests = JSON.parse(provider.active_challenge_requests); + if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { + const parsed = JSON.parse(provider.active_challenge_requests); + if (parsed && typeof parsed === 'object' && 'request_ids' in parsed) { + // Ensure we only include string values in the request_ids array + const validRequestIds = Array.isArray(parsed.request_ids) + ? parsed.request_ids.filter((id: any) => typeof id === 'string') + : []; + parsedChallengeRequests = { request_ids: validRequestIds }; + } } } catch (err) { logger.warn(`${parentLogId} Warning: Failed to parse active_challenge_requests:`, err); } try { - if (provider.active_output_requests) { - //@ts-ignore - parsedOutputRequests = JSON.parse(provider.active_output_requests); + if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { + const parsed = JSON.parse(provider.active_output_requests); + if (parsed && typeof parsed === 'object' && 'request_ids' in parsed) { + // Ensure we only include string values in the request_ids array + const validRequestIds = Array.isArray(parsed.request_ids) + ? parsed.request_ids.filter((id: any) => typeof id === 'string') + : []; + parsedOutputRequests = { request_ids: validRequestIds }; + } } } catch (err) { logger.warn(`${parentLogId} Warning: Failed to parse active_output_requests:`, err); diff --git a/orchestrator/src/monitoring.ts b/orchestrator/src/monitoring.ts index c9545df..5d847dd 100644 --- a/orchestrator/src/monitoring.ts +++ b/orchestrator/src/monitoring.ts @@ -3,9 +3,9 @@ import fs from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; import crypto from 'crypto'; -import { version } from '../package.json'; import logger from './logger'; import { MonitoringData, PerformanceMetrics, SystemSpecs, ExecutionMetrics, HealthStatus } from 'ao-process-clients'; +import { VERSION } from './app'; const execAsync = promisify(exec); @@ -256,7 +256,7 @@ export class MonitoringService { // Construct the full MonitoringData object const monitoringData: MonitoringData = { - providerVersion: version, + providerVersion: VERSION, timestamp: new Date().toISOString(), systemSpecs, performance, diff --git a/requester/package.json b/requester/package.json index 14fb257..35db0f4 100644 --- a/requester/package.json +++ b/requester/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "@permaweb/aoconnect": "^0.0.78", - "ao-process-clients": "^6.0.63", + "ao-process-clients": "^6.0.65", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", diff --git a/requester/src/app.ts b/requester/src/app.ts index 566f1d8..15e1ac4 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -3,7 +3,7 @@ import { } from "ao-process-clients"; //import { TransferToProviders } from "./extra"; -const RETRY_DELAY_MS = 5000; // 1 seconds +const RETRY_DELAY_MS = 1000; // 1 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; @@ -124,7 +124,7 @@ async function main() { const randclient = await getRandomClient() //const stakeclient = ProviderStakingClient.autoConfiguration(); - randclient.prepay(1000000000) + randclient.prepay(1000_000000000) //1,000 while (true) { console.log("Running") try { @@ -135,7 +135,7 @@ async function main() { const { providers, count } = await getRandomProviders(randclient); console.log(`Selected ${count} providers:`, providers); // await randclient.createRequest(providers, count, callbackId); - randclient.redeem(providers, count, callbackId); + console.log(await randclient.redeem(providers, count, callbackId)); //await TransferToProviders(providers, callbackId) //await randclient.createRequest(["X1tqliRkKnClhVQ4aIeyuOaPTzr5PfnxqAoSdpTzZy8"], 1, "123"); totalRandomCalled++; From 4aaa5e67ec909fb9e5e7f7073ac80e49623687bd Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 31 May 2025 11:58:27 -0400 Subject: [PATCH 63/80] v1.0.5 --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index c600ff2..10a1301 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -23,7 +23,7 @@ services: max-file: "5" orchestrator: - image: randao/orchestrator:v1.0.4 + image: randao/orchestrator:v1.0.5 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index b7393aa..5a4479c 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.4 # You can change this value to any version you want +export VERSION=v1.0.5 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index d2ef5d2..02c4cd3 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -5,7 +5,7 @@ import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderReque import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; -export const VERSION = process.env.VERSION || "test"; +export const VERSION = process.env.VERSION || "1.0.4"; export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; From 55931be29fefd716f0ebadb3e2c1c4c8341e147c Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 31 May 2025 13:39:16 -0400 Subject: [PATCH 64/80] v1.0.6 --- orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 2 +- orchestrator/src/helperFunctions.ts | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 5a4479c..6aa240c 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.5 # You can change this value to any version you want +export VERSION=v1.0.6 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 02c4cd3..9d4f27b 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -5,7 +5,7 @@ import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderReque import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; -export const VERSION = process.env.VERSION || "1.0.4"; +export const VERSION = process.env.VERSION || "1.0.6"; export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 274a870..647871c 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -301,16 +301,16 @@ export async function crank() { }); // If there are any defunct requests, run the crank -if (defunctRequestIds.length > 0) { - logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); - (await getRandomClient()).crank(); -} else { - // 1 in 100 chance to crank - if (Math.floor(Math.random() * 100) === 0) { - logger.info("Cranking randomly (1 in 100 chance hit)"); - (await getRandomClient()).crank(); - } -} +// if (defunctRequestIds.length > 0) { +// logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); +// (await getRandomClient()).crank(); +// } else { +// // 1 in 100 chance to crank +// if (Math.floor(Math.random() * 100) === 0) { +// logger.info("Cranking randomly (1 in 100 chance hit)"); +// //(await getRandomClient()).crank(); +// } +// } } export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { From 41777461f93734024e0dad037dd1fcd308c83784 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 31 May 2025 13:39:28 -0400 Subject: [PATCH 65/80] v1.0.6 --- docker-compose/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 10a1301..6cbb2b6 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -23,7 +23,7 @@ services: max-file: "5" orchestrator: - image: randao/orchestrator:v1.0.5 + image: randao/orchestrator:v1.0.6 depends_on: postgres: condition: service_healthy From 9c90401b81122c35819d2d9d0e994ee3e16ecdfc Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 5 Jun 2025 08:10:30 -0400 Subject: [PATCH 66/80] v1.0.6-test1 --- orchestrator/src/helperFunctions.ts | 467 +++++++++++++++++++++------- 1 file changed, 353 insertions(+), 114 deletions(-) diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 647871c..0e4a0b2 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -39,21 +39,71 @@ setInterval(() => { export async function getRandomClient(): Promise { const currentTime = Date.now(); - if (!randomClientInstance || (currentTime - lastInitTime) > REINIT_INTERVAL) { - logger.debug("Initializing RandomClient"); - randomClientInstance = ((await RandomClient.defaultBuilder())) - .withWallet(JSON.parse(process.env.WALLET_JSON!)) - .withAOConfig({ - CU_URL: "https://ur-cu.randao.net", - MU_URL: "https://ur-mu.randao.net", - MODE: "legacy" - }) - .build(); - lastInitTime = currentTime; - logger.debug("RandomClient initialized"); + // If we have a valid instance and it's not time to recreate, return it + if (randomClientInstance && (currentTime - lastInitTime) <= REINIT_INTERVAL) { + return randomClientInstance; } - return randomClientInstance; + // If we're already in the process of creating a new instance, wait for it + if (randomClientInstance === null && lastInitTime > 0) { + const waitStart = Date.now(); + while (Date.now() - waitStart < 30000) { // 30 second timeout + await new Promise(resolve => setTimeout(resolve, 100)); + if (randomClientInstance) { + return randomClientInstance; + } + } + throw new Error('Timeout waiting for RandomClient initialization'); + } + + // Mark that we're creating a new instance + const previousInstance = randomClientInstance; + randomClientInstance = null; + lastInitTime = 0; + + try { + logger.debug("Initializing new RandomClient instance"); + const newClient = await (async () => { + return ((await RandomClient.defaultBuilder())) + .withWallet(JSON.parse(process.env.WALLET_JSON!)) + .withAOConfig({ + CU_URL: "https://ur-cu.randao.net", + MU_URL: "https://ur-mu.randao.net", + MODE: "legacy" + }) + .build(); + })(); + + randomClientInstance = newClient; + lastInitTime = Date.now(); + logger.debug("RandomClient initialized successfully"); + + // Clean up previous instance if it exists + if (previousInstance) { + try { + // Check if disconnect method exists before calling it + if (typeof (previousInstance as any).disconnect === 'function') { + await (previousInstance as any).disconnect().catch((e: Error) => + logger.warn('Error disconnecting previous client:', e) + ); + } + } catch (e) { + logger.warn('Error during previous client cleanup:', e as Error); + } + } + + return newClient; + } catch (error) { + // If we failed to create a new client but had a previous one, keep using it + if (previousInstance) { + logger.error('Failed to create new RandomClient, falling back to previous instance', error); + randomClientInstance = previousInstance; + return previousInstance; + } + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + logger.error('Failed to create RandomClient and no fallback available', errorMessage); + throw new Error(`Failed to initialize RandomClient: ${errorMessage}`); + } } // Step 2: Process Challenge Requests (Database selection & assigning is atomic) @@ -319,9 +369,59 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri activeChallengeRequests: { request_ids: [] }, activeOutputRequests: { request_ids: [] } }; + + let client: RandomClient | null = null; + let response; + try { - const response = await (await getRandomClient()).getAllProviderActivity(); + // Get client with error handling + try { + client = await getRandomClient(); + if (!client) { + throw new Error('Failed to initialize RandomClient'); + } + } catch (clientError) { + logger.error(`${parentLogId} Failed to initialize RandomClient:`, clientError); + return defaultResponse; + } + + // Try to get provider activity with retry logic + const maxRetries = 2; + let lastError: Error | null = null; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + response = await client.getAllProviderActivity(); + lastError = null; + break; // Success, exit retry loop + } catch (error) { + lastError = error as Error; + logger.warn(`${parentLogId} Attempt ${attempt}/${maxRetries} failed to fetch provider activity:`, error); + + if (attempt < maxRetries) { + // Only recreate the client if this isn't the last attempt + try { + randomClientInstance = null; // Force client recreation on next attempt + client = await getRandomClient(); + logger.info(`${parentLogId} Recreated RandomClient for retry attempt ${attempt + 1}`); + } catch (retryError) { + logger.error(`${parentLogId} Failed to recreate RandomClient for retry:`, retryError); + } + // Wait before retrying + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + // If we still have an error after retries, handle it + if (lastError) { + throw lastError; + } + + if (!response) { + throw new Error('No response from provider activity'); + } + // Collect all request IDs from all providers for tracking const allRequestIds: string[] = []; @@ -366,7 +466,7 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri } } } catch (err) { - logger.warn(`${parentLogId} Warning: Failed to process provider ${provider.provider_id}:`, err); + logger.warn(`${parentLogId} Warning: Failed to process provider ${provider?.provider_id || 'unknown'}:`, err); } } @@ -376,7 +476,7 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri // Update the request timestamps tracking logRequestTimestamps(allRequestIds); - const provider = response.find(p => p.provider_id === PROVIDER_ID); + const provider = response.find((p: any) => p.provider_id === PROVIDER_ID); if (!provider) { logger.warn(`${parentLogId} Warning: Provider with ID ${PROVIDER_ID} not found.`); @@ -418,10 +518,12 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri } // Only update current_onchain_random if successful - current_onchain_random = provider.random_balance; + if (typeof provider.random_balance === 'number') { + current_onchain_random = provider.random_balance; + } const result: GetOpenRandomRequestsResponse = { - providerId: provider.provider_id, + providerId: provider.provider_id || PROVIDER_ID, activeChallengeRequests: parsedChallengeRequests, activeOutputRequests: parsedOutputRequests, }; @@ -433,7 +535,19 @@ export async function getProviderRequests(PROVIDER_ID: string, parentLogId: stri return result; } catch (error) { - logger.error(`${parentLogId} Error fetching provider requests: ${error}`); + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`${parentLogId} Error in getProviderRequests:`, errorMessage); + logger.debug(`${parentLogId} Error details:`, error); + + // If we have a client that might be in a bad state, try to clean it up + if (client) { + try { + randomClientInstance = null; + } catch (cleanupError) { + logger.warn(`${parentLogId} Error during client cleanup:`, cleanupError); + } + } + return defaultResponse; } } @@ -536,129 +650,254 @@ export async function updateAvailableValuesAsync(currentCount: number) { // Function to post VDF challenge (fetches dbId dynamically) async function fulfillRandomChallenge(client: Client, requestId: string, parentLogId: string): Promise { - // Check if this request ID is on cooldown - if (challengeCooldowns.get(requestId)) { - logger.debug(`${parentLogId} Skipping challenge for request ID ${requestId} - on cooldown`); + const logPrefix = `${parentLogId} [Challenge ${requestId}]`; + + // Check if this request is already being processed + if (challengeCooldowns.has(requestId)) { + logger.debug(`${logPrefix} Request is in cooldown, skipping...`); return; } - try { - // Mark this request ID as being processed to prevent duplicates - challengeCooldowns.set(requestId, true); + // Set cooldown immediately to prevent concurrent processing + challengeCooldowns.set(requestId, true); + const cooldownTimer = setTimeout(() => { + challengeCooldowns.delete(requestId); + logger.debug(`${logPrefix} Cooldown released`); + }, 60000); // 1 minute cooldown - // Fetch the necessary details from the database using requestId - const res = await client.query( - `SELECT id, modulus, x - FROM time_lock_puzzles - WHERE request_id = $1`, - [requestId] - ); + let randomClient: RandomClient | null = null; + let retryCount = 0; + const maxRetries = 3; + const baseDelay = 1000; // 1 second base delay - if (!res.rowCount) { - logger.error(`No entry found for Request ID: ${requestId}`); - return; - } + while (retryCount <= maxRetries) { + try { + // Get a fresh client for each attempt + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient'); + } + + // Fetch the necessary details from the database using requestId + const res = await client.query( + `SELECT id, modulus, x + FROM time_lock_puzzles + WHERE request_id = $1`, + [requestId] + ); - const { id: dbId, modulus, x: input } = res.rows[0]; + if (!res.rowCount) { + logger.error(`${logPrefix} No database entry found for request`); + return; + } - logger.debug(`${parentLogId} Fetched entry details - Request ID: ${requestId}, DB ID: ${dbId}, Modulus: ${modulus}, Input: ${input}`); - logger.info(`${parentLogId} Posting VDF challenge for Request ID: ${requestId}, DB ID: ${dbId}`); - await (await getRandomClient()).commit({ - requestId: requestId, - puzzle: { - input: input, - modulus: modulus + const { id: dbId, modulus, x: input } = res.rows[0]; + + if (!modulus || !input) { + throw new Error('Missing required fields in database entry'); } - }); - logger.info(`${parentLogId} Challenge posted for Request ID: ${requestId}. Waiting to post proof...`); - - // Set a timeout to release the cooldown after 1 second - setTimeout(() => { - challengeCooldowns.delete(requestId); - logger.debug(`${parentLogId} Challenge cooldown released for request ID: ${requestId}`); - }, 1000); - } catch (error) { - logger.error(`${parentLogId} Error posting VDF challenge for Request ID: ${requestId}:`, error); - // Release the cooldown immediately on error to allow retry - challengeCooldowns.delete(requestId); + + logger.debug(`${logPrefix} Posting VDF challenge for DB ID: ${dbId}`); + + // Post the VDF challenge + await randomClient.commit({ + requestId: requestId, + puzzle: { + input: input, + modulus: modulus + } + }); + + logger.info(`${logPrefix} Successfully posted VDF challenge`); + return; // Success, exit the function + + } catch (error) { + retryCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + + if (retryCount <= maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff + logger.warn(`${logPrefix} Attempt ${retryCount}/${maxRetries} failed, retrying in ${delay}ms:`, errorMessage); + + // Force client recreation on retry + randomClient = null; + randomClientInstance = null; + + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + logger.error(`${logPrefix} All ${maxRetries} attempts failed:`, error); + // The cooldown was already set at the beginning + return; + } + } finally { + // Ensure the client is properly cleaned up + if (randomClient) { + try { + if (typeof (randomClient as any).disconnect === 'function') { + await (randomClient as any).disconnect().catch((e: Error) => + logger.warn(`${logPrefix} Error disconnecting client:`, e) + ); + } + } catch (e) { + logger.warn(`${logPrefix} Error during client cleanup:`, e); + } + } + } } } // Function to post VDF output and proof async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { - // Check if this request ID is on cooldown - if (outputCooldowns.get(requestId)) { - logger.debug(`${parentLogId} Skipping output for request ID ${requestId} - on cooldown`); + const logPrefix = `${parentLogId} [Output ${requestId}]`; + + // Check if this request is already being processed + if (outputCooldowns.has(requestId)) { + logger.debug(`${logPrefix} Request is in cooldown, skipping...`); return; } - try { - // Mark this request ID as being processed to prevent duplicates - outputCooldowns.set(requestId, true); + // Set cooldown immediately to prevent concurrent processing + outputCooldowns.set(requestId, true); + const cooldownTimer = setTimeout(() => { + outputCooldowns.delete(requestId); + logger.debug(`${logPrefix} Cooldown released`); + }, 60000); // 1 minute cooldown - // Fetch the output and proof from the database using the requestId - const res = await client.query( - `SELECT - tlp.id, - tlp.y AS output, - rk.p, - rk.q - FROM time_lock_puzzles tlp - JOIN rsa_keys rk ON tlp.rsa_id = rk.id - WHERE tlp.request_id = $1`, - [requestId] - ); + let randomClient: RandomClient | null = null; + let retryCount = 0; + const maxRetries = 3; + const baseDelay = 1000; // 1 second base delay - if (!res.rowCount) { - logger.error(`No entry found for request ID: ${requestId}`); - return; - } + while (retryCount <= maxRetries) { + try { + // Get a fresh client for each attempt + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient'); + } + + // Fetch the output and proof from the database using the requestId + const res = await client.query( + `SELECT + tlp.id, + tlp.y AS output, + rk.p, + rk.q + FROM time_lock_puzzles tlp + JOIN rsa_keys rk ON tlp.rsa_id = rk.id + WHERE tlp.request_id = $1`, + [requestId] + ); - // Map the response to structured variables - const { - id: dbId, - output, // Mapping 'y' to 'output' - p: rsaP, - q: rsaQ - } = res.rows[0]; - - logger.debug(`${parentLogId} Fetched entry from database for output - ID: ${dbId}, requestID: ${requestId}, output: ${output}, rsaP: ${rsaP}, rsaQ ${rsaQ} `); - - logger.info(`${parentLogId} Posting VDF output and proof for - ID: ${dbId}, request ID: ${requestId}`); - await (await getRandomClient()).reveal({ - requestId: requestId, - rsa_key: { - p: rsaP, - q: rsaQ + if (!res.rowCount) { + logger.error(`${logPrefix} No database entry found for request`); + return; } - }); - logger.info(`${parentLogId} Proof posted for request ID: ${requestId}`); - - // Set a timeout to release the cooldown after 1 second - setTimeout(() => { - outputCooldowns.delete(requestId); - logger.debug(`${parentLogId} Output cooldown released for request ID: ${requestId}`); - }, 1000); - } catch (error) { - logger.error(`${parentLogId} Error fulfilling random output for request ID: ${requestId}:`, error); - // Release the cooldown immediately on error to allow retry - outputCooldowns.delete(requestId); + + + const { id: dbId, output, p: rsaP, q: rsaQ } = res.rows[0]; + + if (!output || !rsaP || !rsaQ) { + throw new Error('Missing required fields in database entry'); + } + + logger.debug(`${logPrefix} Posting VDF output and proof for DB ID: ${dbId}`); + + // Post the VDF output and proof + await randomClient.reveal({ + requestId: requestId, + rsa_key: { + p: rsaP, + q: rsaQ + } + }); + + logger.info(`${logPrefix} Successfully posted VDF output and proof`); + return; // Success, exit the function + + } catch (error) { + retryCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + + if (retryCount <= maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff + logger.warn(`${logPrefix} Attempt ${retryCount}/${maxRetries} failed, retrying in ${delay}ms:`, errorMessage); + + // Force client recreation on retry + randomClient = null; + randomClientInstance = null; + + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + logger.error(`${logPrefix} All ${maxRetries} attempts failed:`, error); + // The cooldown was already set at the beginning + return; + } + } finally { + // Ensure the client is properly cleaned up + if (randomClient) { + try { + if (typeof (randomClient as any).disconnect === 'function') { + await (randomClient as any).disconnect().catch((e: Error) => + logger.warn(`${logPrefix} Error disconnecting client:`, e) + ); + } + } catch (e) { + logger.warn(`${logPrefix} Error during client cleanup:`, e); + } + } + } } } export async function shutdown() { - //TODO do post 0 avalible random then do like as much polling as you can before turning off to ensure all random gets pushed through + const logPrefix = '[Shutdown]'; + let randomClient: RandomClient | null = null; + try { - // // Get monitoring data for final update - // const monitoringData = await monitoring.getMonitoringData(); - + logger.info(`${logPrefix} Starting graceful shutdown sequence`); + + // Get monitoring data for final update + const monitoringData = await monitoring.getMonitoringData(); + + // Get a fresh client for the shutdown sequence + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient during shutdown'); + } + // Set provider available values to 0 and include final monitoring data - const message = await (await getRandomClient()).updateProviderAvailableValues(0); - logger.info(String(message)); // Convert message to string - logger.info(`Updated provider values to 0... shutting down ...`); + logger.info(`${logPrefix} Updating provider values to 0...`); + await randomClient.updateProviderAvailableValues(0, monitoringData); + + logger.info(`${logPrefix} Provider values updated to 0`); + + // Add a small delay to ensure the update is processed + await new Promise(resolve => setTimeout(resolve, 2000)); + + logger.info(`${logPrefix} Shutdown sequence completed`); + } catch (error) { - logger.error("Failed to update provider values:", error); + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`${logPrefix} Error during shutdown:`, errorMessage); + logger.debug(`${logPrefix} Error details:`, error); monitoring.incrementErrorCount(); + } finally { + // Ensure the client is properly cleaned up + if (randomClient) { + try { + if (typeof (randomClient as any).disconnect === 'function') { + await (randomClient as any).disconnect().catch((e: Error) => + logger.warn(`${logPrefix} Error disconnecting client:`, e) + ); + } + } catch (e) { + logger.warn(`${logPrefix} Error during client cleanup:`, e); + } + } + + // Clear the client instance to ensure a fresh start if the process continues + randomClientInstance = null; } } From 4e57438bf4d4de10a7984a6c9a3c3852137edce4 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 6 Jun 2025 12:47:26 -0400 Subject: [PATCH 67/80] v1.0.7 --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/package.json | 2 +- orchestrator/src/app.ts | 2 +- orchestrator/src/helper2old.ts | 903 ++++++++++++++++++++++++++++++ requester/package.json | 2 +- 6 files changed, 908 insertions(+), 5 deletions(-) create mode 100644 orchestrator/src/helper2old.ts diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 6cbb2b6..ea74f03 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -23,7 +23,7 @@ services: max-file: "5" orchestrator: - image: randao/orchestrator:v1.0.6 + image: randao/orchestrator:v1.0.7 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 6aa240c..153ae9d 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.6 # You can change this value to any version you want +export VERSION=v1.0.7 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/package.json b/orchestrator/package.json index 20cacb0..5dfd4af 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -7,7 +7,7 @@ "typescript": "^5.6.3" }, "dependencies": { - "ao-process-clients": "^6.0.59", + "ao-process-clients": "^6.0.67", "ao-vrf": "file:", "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 9d4f27b..1e424c6 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -5,7 +5,7 @@ import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderReque import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; -export const VERSION = process.env.VERSION || "1.0.6"; +export const VERSION = process.env.VERSION || "1.0.7"; export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; diff --git a/orchestrator/src/helper2old.ts b/orchestrator/src/helper2old.ts new file mode 100644 index 0000000..56f499e --- /dev/null +++ b/orchestrator/src/helper2old.ts @@ -0,0 +1,903 @@ +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RequestList } from "ao-process-clients"; +import { Client } from "pg"; +import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, MINIMUM_RANDOM_DELTA, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; +import { getMoreRandom, monitorDockerContainers } from "./containerManagment"; +import logger, { LogLevel } from "./logger"; +import { monitoring } from "./monitoring"; +import { setTimeout, setInterval } from 'timers'; + +let randomClientInstance: RandomClient | null = null; +let lastInitTime: number = 0; +const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds +let current_onchain_random = - 10 + +// Cooldown tracking for updateAvailableValuesAsync +let lastUpdatedOnChainTime = 0; +const FIFTEEN_MINUTES_MS = 15 * 60 * 1000; + +// Cooldown tracking for fulfillRandomChallenge and fulfillRandomOutput (per request ID) +const challengeCooldowns = new Map(); +const outputCooldowns = new Map(); +// Track whether there's an ongoing random generation request +let ongoingRandomGeneration = false; +const MAX_RANDOM_PER_REQUEST = 500; // Maximum number of random values to generate in a single request + +// Map to track request timestamps +const requestTimestamps: Map = new Map(); + +// Function to reset the ongoingRandomGeneration flag +export function resetOngoingRandomGeneration() { + ongoingRandomGeneration = false; + logger.info('Random generation flag reset. System ready for new random generation requests.'); +} + +// Optional: Auto-reinitialize on a timer +setInterval(() => { + randomClientInstance = null; +}, REINIT_INTERVAL); + +export async function getRandomClient(): Promise { + const currentTime = Date.now(); + + // If we have a valid instance and it's not time to recreate, return it + if (randomClientInstance && (currentTime - lastInitTime) <= REINIT_INTERVAL) { + return randomClientInstance; + } + + // If we're already in the process of creating a new instance, wait for it + if (randomClientInstance === null && lastInitTime > 0) { + const waitStart = Date.now(); + while (Date.now() - waitStart < 30000) { // 30 second timeout + await new Promise(resolve => setTimeout(resolve, 100)); + if (randomClientInstance) { + return randomClientInstance; + } + } + throw new Error('Timeout waiting for RandomClient initialization'); + } + + // Mark that we're creating a new instance + const previousInstance = randomClientInstance; + randomClientInstance = null; + lastInitTime = 0; + + try { + logger.debug("Initializing new RandomClient instance"); + const newClient = await (async () => { + return ((await RandomClient.defaultBuilder())) + .withWallet(JSON.parse(process.env.WALLET_JSON!)) + .withAOConfig({ + CU_URL: "https://ur-cu.randao.net", + MU_URL: "https://ur-mu.randao.net", + MODE: "legacy" + }) + .build(); + })(); + + randomClientInstance = newClient; + lastInitTime = Date.now(); + logger.debug("RandomClient initialized successfully"); + + // Clean up previous instance if it exists + if (previousInstance) { + try { + // Check if disconnect method exists before calling it + if (typeof (previousInstance as any).disconnect === 'function') { + await (previousInstance as any).disconnect().catch((e: Error) => + logger.warn('Error disconnecting previous client:', e) + ); + } + } catch (e) { + logger.warn('Error during previous client cleanup:', e as Error); + } + } + + return newClient; + } catch (error) { + // If we failed to create a new client but had a previous one, keep using it + if (previousInstance) { + logger.error('Failed to create new RandomClient, falling back to previous instance', error); + randomClientInstance = previousInstance; + return previousInstance; + } + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + logger.error('Failed to create RandomClient and no fallback available', errorMessage); + throw new Error(`Failed to initialize RandomClient: ${errorMessage}`); + } +} + +// Step 2: Process Challenge Requests (Database selection & assigning is atomic) +export async function processChallengeRequests( + client: Client, + activeChallengeRequests: { request_ids: string[] } | undefined, + parentLogId: string +): Promise { + logger.info(`${parentLogId} Step 2: Processing challenge requests.`); + + if (!activeChallengeRequests || activeChallengeRequests.request_ids.length === 0) { + logger.info(`${parentLogId} No Challenge Requests to process.`); + return; + } + + const requestIds = activeChallengeRequests.request_ids; + logger.info(`${parentLogId} Processing up to ${requestIds.length} requests.`); + + try { + await client.query('BEGIN'); // Start transaction + + logger.debug(`${parentLogId} Fetching existing request mappings.`); + + // Fetch already assigned request_id -> dbId mappings + const existingMappingsRes = await client.query( + `SELECT request_id FROM time_lock_puzzles + WHERE request_id = ANY($1) + FOR UPDATE SKIP LOCKED`, + [requestIds] + ); + + const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); + logger.debug(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); + + // Find only the unmapped requests (requestIds not in existingRequestIds) + const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); + logger.debug(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); + + let mappedEntries: { requestId: string, dbId: number }[] = []; + + if (unmappedRequestIds.length > 0) { + logger.debug(`${parentLogId} Fetching available DB entries.`); + const dbRes = await client.query( + `SELECT id FROM time_lock_puzzles + WHERE request_id IS NULL + ORDER BY id ASC + LIMIT $1 + FOR UPDATE SKIP LOCKED`, + [unmappedRequestIds.length] + ); + + const availableDbEntries = dbRes.rows.map(row => row.id); + logger.debug(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); + + if (availableDbEntries.length > 0) { + const numMappings = Math.min(unmappedRequestIds.length, availableDbEntries.length); + + for (let i = 0; i < numMappings; i++) { + await client.query( + `UPDATE time_lock_puzzles + SET request_id = $1 + WHERE id = $2`, + [unmappedRequestIds[i], availableDbEntries[i]] + ); + mappedEntries.push({ requestId: unmappedRequestIds[i], dbId: availableDbEntries[i] }); + logger.debug(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); + } + } else { + logger.warn(`${parentLogId} No available DB entries for unmapped requests.`); + } + } + + // Collect all request IDs (previously mapped + newly mapped) + const allRequestIds = [...existingRequestIds, ...mappedEntries.map(entry => entry.requestId)]; + + if (allRequestIds.length === 0) { + logger.info(`${parentLogId} No requests to process. Committing transaction.`); + await client.query('COMMIT'); + return; + } + + await client.query('COMMIT'); // Commit all updates at once + logger.info(`${parentLogId} Committed all changes. Now fulfilling challenges.`); + + // Call fulfillRandomChallenge for all request IDs + await Promise.all( + allRequestIds.map(requestId => + fulfillRandomChallenge(client, requestId, parentLogId) + .catch(error => logger.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) + ) + ); + + logger.info(`${parentLogId} All challenges fulfilled`); + } catch (error: any) { + logger.error(`${parentLogId} Error in processChallengeRequests:`, error); + await client.query('ROLLBACK'); // Rollback on failure + + logger.error(`SQL State: ${error.code}, Message: ${error.message}`); + } +} + +// Step 3: Process Output Requests +export async function processOutputRequests( + client: Client, + activeOutputRequests: { request_ids: string[] } | undefined, + parentLogId: string +): Promise { + logger.info(`${parentLogId} Step 3: Processing output requests.`); + + if (!activeOutputRequests || activeOutputRequests.request_ids.length === 0) { + logger.info(`${parentLogId} No Output Requests to process.`); + return; + } + + const outputPromises = activeOutputRequests.request_ids.map(async (requestId) => { + logger.debug(`${parentLogId} Processing output request ID: ${requestId}`); + + // Run fulfillRandomOutput asynchronously (do not await) + fulfillRandomOutput(client, requestId, parentLogId) + .catch(error => logger.error(`${parentLogId} Error fulfilling output:`, error)); + }); + + await Promise.all(outputPromises); + logger.info(`${parentLogId} Step 3 completed.`); +} + +// Step 4: Remove fulfilled entries no longer in use +export async function cleanupFulfilledEntries( + client: Client, + openRequests: any, + parentLogId: string +): Promise { + logger.info(`${parentLogId} Step 4: Checking for fulfilled entries no longer in use.`); + + const now = new Date(); + const cutoffTime = new Date(now.getTime() - COMPLETION_RETENTION_PERIOD_MS); + + try { + await client.query('BEGIN'); + + // Fetch all entries with a request_id + const result = await client.query(` + SELECT id, request_id, detected_completed + FROM time_lock_puzzles + WHERE request_id IS NOT NULL + `); + + let markForDeletion: string[] = []; + let markAsCompleted: string[] = []; + + for (const row of result.rows) { + const { id, request_id, detected_completed } = row; + + // Check if this request is still active in challenge or output + const isStillInChallenge = openRequests.activeChallengeRequests?.request_ids.includes(request_id); + const isStillInOutput = openRequests.activeOutputRequests?.request_ids.includes(request_id); + + if (!isStillInChallenge && !isStillInOutput) { + if (!detected_completed) { + // Mark it for deletion by setting detected_completed timestamp + markAsCompleted.push(id); + } else if (new Date(detected_completed) < cutoffTime) { + // If already marked and older than retention period, delete it + markForDeletion.push(id); + } + } + } + + // Mark entries as completed + if (markAsCompleted.length > 0) { + await client.query(` + UPDATE time_lock_puzzles + SET detected_completed = NOW() + WHERE id = ANY($1) + `, [markAsCompleted]); + logger.debug(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); + } + + // Delete old completed entries + if (markForDeletion.length > 0) { + await client.query(` + DELETE FROM rsa_keys + WHERE id IN ( + SELECT rsa_id FROM time_lock_puzzles WHERE id = ANY($1) + ); + `, [markForDeletion]); + + await client.query(` + DELETE FROM time_lock_puzzles + WHERE id = ANY($1); + `, [markForDeletion]); + + logger.info(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); + } + + await client.query('COMMIT'); + } catch (error) { + logger.error(`${parentLogId} Error in cleanupFulfilledEntries:`, error); + await client.query('ROLLBACK'); + } + + logger.info(`${parentLogId} Step 4 completed.`); +} + + +/** + * Function to log request timestamps + * Adds new request IDs to the tracking map and removes ones that are no longer present + * @param allRequestIds Array of request IDs to track + */ +export function logRequestTimestamps(allRequestIds: string[]): void { + const currentTime = Date.now(); + const existingIds = new Set(requestTimestamps.keys()); + + // Add new request IDs with current timestamp + for (const requestId of allRequestIds) { + if (!requestTimestamps.has(requestId)) { + logger.verbose(`Adding new request ID to tracking: ${requestId}`); + requestTimestamps.set(requestId, currentTime); + } + } + + // Remove request IDs that are no longer present + for (const existingId of existingIds) { + if (!allRequestIds.includes(existingId)) { + logger.verbose(`Removing request ID from tracking: ${existingId}`); + requestTimestamps.delete(existingId); + } + } +} + +// Function to check for defunct requests and crank if needed +export async function crank() { + const currentTime = Date.now(); + const defunctRequestIds: string[] = []; + const DEFUNCT_THRESHOLD_MS = 30 * 1000; // 30 seconds + + // Check for defunct request IDs (those that have been in the map for over 30 seconds) + requestTimestamps.forEach((timestamp, requestId) => { + const timeInMap = currentTime - timestamp; + if (timeInMap > DEFUNCT_THRESHOLD_MS) { + defunctRequestIds.push(requestId); + logger.info(`Defunct request found: ${requestId} (in system for ${Math.floor(timeInMap / 1000)} seconds)`); + } + }); + + //If there are any defunct requests, run the crank +// if (defunctRequestIds.length > 0) { +// logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); +// (await getRandomClient()).crank(); +// } else { +// // 1 in 100 chance to crank +// if (Math.floor(Math.random() * 100) === 0) { +// logger.info("Cranking randomly (1 in 100 chance hit)"); +// //(await getRandomClient()).crank(); +// } +// } +} + +export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { + const defaultResponse: GetOpenRandomRequestsResponse = { + providerId: PROVIDER_ID, + activeChallengeRequests: { request_ids: [] }, + activeOutputRequests: { request_ids: [] } + }; + + let client: RandomClient | null = null; + let response; + + try { + // Get client with error handling + try { + client = await getRandomClient(); + if (!client) { + throw new Error('Failed to initialize RandomClient'); + } + } catch (clientError) { + logger.error(`${parentLogId} Failed to initialize RandomClient:`, clientError); + return defaultResponse; + } + + // Try to get provider activity with retry logic + const maxRetries = 2; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + response = await client.getAllProviderActivity(); + lastError = null; + break; // Success, exit retry loop + } catch (error) { + lastError = error as Error; + logger.warn(`${parentLogId} Attempt ${attempt}/${maxRetries} failed to fetch provider activity:`, error); + + if (attempt < maxRetries) { + // Only recreate the client if this isn't the last attempt + try { + randomClientInstance = null; // Force client recreation on next attempt + client = await getRandomClient(); + logger.info(`${parentLogId} Recreated RandomClient for retry attempt ${attempt + 1}`); + } catch (retryError) { + logger.error(`${parentLogId} Failed to recreate RandomClient for retry:`, retryError); + } + // Wait before retrying + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + // If we still have an error after retries, handle it + if (lastError) { + throw lastError; + } + + if (!response) { + throw new Error('No response from provider activity'); + } + + // Collect all request IDs from all providers for tracking + const allRequestIds: string[] = []; + + // Process each provider to extract request IDs + for (const provider of response) { + try { + // Extract challenge request IDs + if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { + try { + const parsedChallengeData = JSON.parse(provider.active_challenge_requests); + if (parsedChallengeData && typeof parsedChallengeData === 'object' && 'request_ids' in parsedChallengeData) { + const requestIds = parsedChallengeData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + logger.warn(`${parentLogId} Warning: Failed to parse challenge requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + + // Extract output request IDs + if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { + try { + const parsedOutputData = JSON.parse(provider.active_output_requests); + if (parsedOutputData && typeof parsedOutputData === 'object' && 'request_ids' in parsedOutputData) { + const requestIds = parsedOutputData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + logger.warn(`${parentLogId} Warning: Failed to parse output requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + } catch (err) { + logger.warn(`${parentLogId} Warning: Failed to process provider ${provider?.provider_id || 'unknown'}:`, err); + } + } + + // Log all the request IDs for tracking + logger.debug(`${parentLogId} Found ${allRequestIds.length} request IDs across all providers`); + + // Update the request timestamps tracking + logRequestTimestamps(allRequestIds); + + const provider = response.find((p: any) => p.provider_id === PROVIDER_ID); + + if (!provider) { + logger.warn(`${parentLogId} Warning: Provider with ID ${PROVIDER_ID} not found.`); + return defaultResponse; + } + + // Attempt to parse fields if they exist, otherwise default to empty arrays + let parsedChallengeRequests: RequestList = { request_ids: [] }; + let parsedOutputRequests: RequestList = { request_ids: [] }; + + try { + if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { + const parsed = JSON.parse(provider.active_challenge_requests); + if (parsed && typeof parsed === 'object' && 'request_ids' in parsed) { + // Ensure we only include string values in the request_ids array + const validRequestIds = Array.isArray(parsed.request_ids) + ? parsed.request_ids.filter((id: any) => typeof id === 'string') + : []; + parsedChallengeRequests = { request_ids: validRequestIds }; + } + } + } catch (err) { + logger.warn(`${parentLogId} Warning: Failed to parse active_challenge_requests:`, err); + } + + try { + if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { + const parsed = JSON.parse(provider.active_output_requests); + if (parsed && typeof parsed === 'object' && 'request_ids' in parsed) { + // Ensure we only include string values in the request_ids array + const validRequestIds = Array.isArray(parsed.request_ids) + ? parsed.request_ids.filter((id: any) => typeof id === 'string') + : []; + parsedOutputRequests = { request_ids: validRequestIds }; + } + } + } catch (err) { + logger.warn(`${parentLogId} Warning: Failed to parse active_output_requests:`, err); + } + + // Only update current_onchain_random if successful + if (typeof provider.random_balance === 'number') { + current_onchain_random = provider.random_balance; + } + + const result: GetOpenRandomRequestsResponse = { + providerId: provider.provider_id || PROVIDER_ID, + activeChallengeRequests: parsedChallengeRequests, + activeOutputRequests: parsedOutputRequests, + }; + + logger.verbose(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(result)}`); + logger.info(`${parentLogId} Step 1: Open Challenge Requests count: ${result.activeChallengeRequests.request_ids.length}`); + logger.info(`${parentLogId} Step 1: Open Output Requests count: ${result.activeOutputRequests.request_ids.length}`); + + return result; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`${parentLogId} Error in getProviderRequests:`, errorMessage); + logger.debug(`${parentLogId} Error details:`, error); + + // If we have a client that might be in a bad state, try to clean it up + if (client) { + try { + randomClientInstance = null; + } catch (cleanupError) { + logger.warn(`${parentLogId} Error during client cleanup:`, cleanupError); + } + } + + return defaultResponse; + } +} + +// Function to check and fetch database entries as needed +export async function checkAndFetchIfNeeded(client: Client) { + try { + // Query current count of usable DB entries + const res = await client.query( + 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' + ); + const currentCount = parseInt(res.rows[0].count, 10); + logger.info(`Total usable DB entries: ${currentCount}`); + + switch (current_onchain_random) { + case -1: + logger.warn("Value is -1"); + logger.warn("Provider has been shut down by USER..."); + logger.warn("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-1); + break; + case -2: + logger.error("Value is -2"); + logger.error("Provider has been shut down by PROCESS..."); + logger.error("This is due to One of the following: "); + logger.error("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); + logger.error("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)"); + logger.error("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); + logger.error("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-2); + break; + case -3: + logger.warn("Value is -3"); + logger.warn("Provider has been shut down by PROCESS..."); + logger.warn("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); + logger.warn("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-3); + break; + case -10: + logger.info("Value is -10"); + logger.info("Provider has been turned on and is starting up OR is not staked yet"); + logger.info("Go to the provider dashboard to Stake if you have not yet OR wait for provider to finish turning on if you have staked already"); + break; + default: + logger.debug("Provider is up and working"); + logger.debug(`Onchain Value is ${current_onchain_random}`); + logger.debug(`Local Value is ${currentCount}`); + await updateAvailableValuesAsync(currentCount); + + } + + // First check if a random generation is already in progress. If so, see if it's done + if (ongoingRandomGeneration) { + logger.info('A random generation process is already running. Skipping new request.'); + await monitorDockerContainers(); + return; + } + + // Check if more entries are needed + const entriesNeeded = MINIMUM_ENTRIES - currentCount; + if (entriesNeeded < MINIMUM_RANDOM_DELTA) return; + + // Set the flag to prevent concurrent random generation + ongoingRandomGeneration = true; + + // Limit to MAX_RANDOM_PER_REQUEST + const randomToGenerate = Math.min(entriesNeeded, MAX_RANDOM_PER_REQUEST); + + logger.info(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${randomToGenerate} entries (out of ${entriesNeeded} needed)...`); + + // Start the random generation with the calculated amount + await getMoreRandom(randomToGenerate); + + } catch (error) { + logger.error('Error during check and fetch:', error); + ongoingRandomGeneration = false; // Reset the flag on error + } +} + +export async function updateAvailableValuesAsync(currentCount: number) { + const now = Date.now(); + + if (now - lastUpdatedOnChainTime < FIFTEEN_MINUTES_MS) { + logger.info(`On-chain update skipped - only ${Math.floor((now - lastUpdatedOnChainTime) / 1000)}s since last update`); + return; + } + + try { + const monitoringData = await monitoring.getMonitoringData(); + + await (await getRandomClient()).updateProviderAvailableValues(currentCount, monitoringData); + logger.info(`Updated provider values to ${currentCount}`); + + lastUpdatedOnChainTime = now; + } catch (error) { + logger.error("Failed to update provider values:", error); + monitoring.incrementErrorCount(); + } +} + +// Function to post VDF challenge (fetches dbId dynamically) +async function fulfillRandomChallenge(client: Client, requestId: string, parentLogId: string): Promise { + const logPrefix = `${parentLogId} [Challenge ${requestId}]`; + + // Check if this request is already being processed + if (challengeCooldowns.has(requestId)) { + logger.debug(`${logPrefix} Request is in cooldown, skipping...`); + return; + } + + // Set cooldown immediately to prevent concurrent processing + challengeCooldowns.set(requestId, true); + const cooldownTimer = setTimeout(() => { + challengeCooldowns.delete(requestId); + logger.debug(`${logPrefix} Cooldown released`); + }, 60000); // 1 minute cooldown + + let randomClient: RandomClient | null = null; + let retryCount = 0; + const maxRetries = 3; + const baseDelay = 1000; // 1 second base delay + + while (retryCount <= maxRetries) { + try { + // Get a fresh client for each attempt + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient'); + } + + // Fetch the necessary details from the database using requestId + const res = await client.query( + `SELECT id, modulus, x + FROM time_lock_puzzles + WHERE request_id = $1`, + [requestId] + ); + + if (!res.rowCount) { + logger.error(`${logPrefix} No database entry found for request`); + return; + } + + + const { id: dbId, modulus, x: input } = res.rows[0]; + + if (!modulus || !input) { + throw new Error('Missing required fields in database entry'); + } + + logger.debug(`${logPrefix} Posting VDF challenge for DB ID: ${dbId}`); + + // Post the VDF challenge + await randomClient.commit({ + requestId: requestId, + puzzle: { + input: input, + modulus: modulus + } + }); + + logger.info(`${logPrefix} Successfully posted VDF challenge`); + return; // Success, exit the function + + } catch (error) { + retryCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + + if (retryCount <= maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff + logger.warn(`${logPrefix} Attempt ${retryCount}/${maxRetries} failed, retrying in ${delay}ms:`, errorMessage); + + // Force client recreation on retry + randomClient = null; + randomClientInstance = null; + + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + logger.error(`${logPrefix} All ${maxRetries} attempts failed:`, error); + // The cooldown was already set at the beginning + return; + } + } finally { + // Ensure the client is properly cleaned up + if (randomClient) { + try { + if (typeof (randomClient as any).disconnect === 'function') { + await (randomClient as any).disconnect().catch((e: Error) => + logger.warn(`${logPrefix} Error disconnecting client:`, e) + ); + } + } catch (e) { + logger.warn(`${logPrefix} Error during client cleanup:`, e); + } + } + } + } +} + +// Function to post VDF output and proof +async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { + const logPrefix = `${parentLogId} [Output ${requestId}]`; + + // Check if this request is already being processed + if (outputCooldowns.has(requestId)) { + logger.debug(`${logPrefix} Request is in cooldown, skipping...`); + return; + } + + // Set cooldown immediately to prevent concurrent processing + outputCooldowns.set(requestId, true); + const cooldownTimer = setTimeout(() => { + outputCooldowns.delete(requestId); + logger.debug(`${logPrefix} Cooldown released`); + }, 60000); // 1 minute cooldown + + let randomClient: RandomClient | null = null; + let retryCount = 0; + const maxRetries = 3; + const baseDelay = 1000; // 1 second base delay + + while (retryCount <= maxRetries) { + try { + // Get a fresh client for each attempt + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient'); + } + + // Fetch the output and proof from the database using the requestId + const res = await client.query( + `SELECT + tlp.id, + tlp.y AS output, + rk.p, + rk.q + FROM time_lock_puzzles tlp + JOIN rsa_keys rk ON tlp.rsa_id = rk.id + WHERE tlp.request_id = $1`, + [requestId] + ); + + if (!res.rowCount) { + logger.error(`${logPrefix} No database entry found for request`); + return; + } + + + const { id: dbId, output, p: rsaP, q: rsaQ } = res.rows[0]; + + if (!output || !rsaP || !rsaQ) { + throw new Error('Missing required fields in database entry'); + } + + logger.debug(`${logPrefix} Posting VDF output and proof for DB ID: ${dbId}`); + + // Post the VDF output and proof + await randomClient.reveal({ + requestId: requestId, + rsa_key: { + p: rsaP, + q: rsaQ + } + }); + + logger.info(`${logPrefix} Successfully posted VDF output and proof`); + return; // Success, exit the function + + } catch (error) { + retryCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + + if (retryCount <= maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff + logger.warn(`${logPrefix} Attempt ${retryCount}/${maxRetries} failed, retrying in ${delay}ms:`, errorMessage); + + // Force client recreation on retry + randomClient = null; + randomClientInstance = null; + + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + logger.error(`${logPrefix} All ${maxRetries} attempts failed:`, error); + // The cooldown was already set at the beginning + return; + } + } finally { + // Ensure the client is properly cleaned up + if (randomClient) { + try { + if (typeof (randomClient as any).disconnect === 'function') { + await (randomClient as any).disconnect().catch((e: Error) => + logger.warn(`${logPrefix} Error disconnecting client:`, e) + ); + } + } catch (e) { + logger.warn(`${logPrefix} Error during client cleanup:`, e); + } + } + } + } +} + +export async function shutdown() { + const logPrefix = '[Shutdown]'; + let randomClient: RandomClient | null = null; + + try { + logger.info(`${logPrefix} Starting graceful shutdown sequence`); + + // Get monitoring data for final update + const monitoringData = await monitoring.getMonitoringData(); + + // Get a fresh client for the shutdown sequence + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient during shutdown'); + } + + // Set provider available values to 0 and include final monitoring data + logger.info(`${logPrefix} Updating provider values to 0...`); + await randomClient.updateProviderAvailableValues(0, monitoringData); + + logger.info(`${logPrefix} Provider values updated to 0`); + + // Add a small delay to ensure the update is processed + await new Promise(resolve => setTimeout(resolve, 2000)); + + logger.info(`${logPrefix} Shutdown sequence completed`); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`${logPrefix} Error during shutdown:`, errorMessage); + logger.debug(`${logPrefix} Error details:`, error); + monitoring.incrementErrorCount(); + } finally { + // Ensure the client is properly cleaned up + if (randomClient) { + try { + if (typeof (randomClient as any).disconnect === 'function') { + await (randomClient as any).disconnect().catch((e: Error) => + logger.warn(`${logPrefix} Error disconnecting client:`, e) + ); + } + } catch (e) { + logger.warn(`${logPrefix} Error during client cleanup:`, e); + } + } + + // Clear the client instance to ensure a fresh start if the process continues + randomClientInstance = null; + } +} diff --git a/requester/package.json b/requester/package.json index 35db0f4..4e81048 100644 --- a/requester/package.json +++ b/requester/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "@permaweb/aoconnect": "^0.0.78", - "ao-process-clients": "^6.0.65", + "ao-process-clients": "^6.0.67", "ao-vrf": "file:", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", From 44fae1824c9a0261ad656bf9dc19197bcc3713ed Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 13 Jun 2025 14:42:24 -0400 Subject: [PATCH 68/80] v8 --- README.md | 13 +- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 2 +- orchestrator/src/helper2old.ts | 903 ---------------------------- orchestrator/src/helperFunctions.ts | 114 ++-- requester/src/app.ts | 12 +- requester/src/extra.ts | 64 -- requester/src/requestTracker.ts | 171 ++++++ 9 files changed, 236 insertions(+), 1047 deletions(-) delete mode 100644 orchestrator/src/helper2old.ts delete mode 100644 requester/src/extra.ts create mode 100644 requester/src/requestTracker.ts diff --git a/README.md b/README.md index 144bcf3..87e08f5 100644 --- a/README.md +++ b/README.md @@ -86,14 +86,11 @@ To run a node, you'll need: Then edit the `.env` file and fill in all required variables: **Required Variables:** - - `provider_id`: Your unique provider identifier - - `local_db_user`: Database username - - `local_db_password`: Database password - - `local_wallet_json`: Your wallet information (either directly pasted or as a file path) - - **Optional Variables (with defaults):** - - `db_name`: Database name (default: orchestrator_db) - - `secrets_prefix`: Prefix for secrets (default: /orchestrator) + - `local_db_user`: Database username (optional to change) + - `local_db_password`: Database password (optional to change) + - `db_name`: Database name (optional to change) + - `LOG_CONSOLE_LEVEL`: The log level you want to see + - `local_wallet_json`: Your wallet information 2. **Deploy Your Provider** From the Docker Compose directory, run: diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index ea74f03..e2a5719 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -23,7 +23,7 @@ services: max-file: "5" orchestrator: - image: randao/orchestrator:v1.0.7 + image: randao/orchestrator:v1.0.8 depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index 153ae9d..af5b38e 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.7 # You can change this value to any version you want +export VERSION=v1.0.8 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 1e424c6..d2dca95 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -5,7 +5,7 @@ import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderReque import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; -export const VERSION = process.env.VERSION || "1.0.7"; +export const VERSION = process.env.VERSION || "1.0.8"; export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; diff --git a/orchestrator/src/helper2old.ts b/orchestrator/src/helper2old.ts deleted file mode 100644 index 56f499e..0000000 --- a/orchestrator/src/helper2old.ts +++ /dev/null @@ -1,903 +0,0 @@ -import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RequestList } from "ao-process-clients"; -import { Client } from "pg"; -import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, MINIMUM_RANDOM_DELTA, UNCHAIN_VS_OFFCHAIN_MAX_DIF } from "./app"; -import { getMoreRandom, monitorDockerContainers } from "./containerManagment"; -import logger, { LogLevel } from "./logger"; -import { monitoring } from "./monitoring"; -import { setTimeout, setInterval } from 'timers'; - -let randomClientInstance: RandomClient | null = null; -let lastInitTime: number = 0; -const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds -let current_onchain_random = - 10 - -// Cooldown tracking for updateAvailableValuesAsync -let lastUpdatedOnChainTime = 0; -const FIFTEEN_MINUTES_MS = 15 * 60 * 1000; - -// Cooldown tracking for fulfillRandomChallenge and fulfillRandomOutput (per request ID) -const challengeCooldowns = new Map(); -const outputCooldowns = new Map(); -// Track whether there's an ongoing random generation request -let ongoingRandomGeneration = false; -const MAX_RANDOM_PER_REQUEST = 500; // Maximum number of random values to generate in a single request - -// Map to track request timestamps -const requestTimestamps: Map = new Map(); - -// Function to reset the ongoingRandomGeneration flag -export function resetOngoingRandomGeneration() { - ongoingRandomGeneration = false; - logger.info('Random generation flag reset. System ready for new random generation requests.'); -} - -// Optional: Auto-reinitialize on a timer -setInterval(() => { - randomClientInstance = null; -}, REINIT_INTERVAL); - -export async function getRandomClient(): Promise { - const currentTime = Date.now(); - - // If we have a valid instance and it's not time to recreate, return it - if (randomClientInstance && (currentTime - lastInitTime) <= REINIT_INTERVAL) { - return randomClientInstance; - } - - // If we're already in the process of creating a new instance, wait for it - if (randomClientInstance === null && lastInitTime > 0) { - const waitStart = Date.now(); - while (Date.now() - waitStart < 30000) { // 30 second timeout - await new Promise(resolve => setTimeout(resolve, 100)); - if (randomClientInstance) { - return randomClientInstance; - } - } - throw new Error('Timeout waiting for RandomClient initialization'); - } - - // Mark that we're creating a new instance - const previousInstance = randomClientInstance; - randomClientInstance = null; - lastInitTime = 0; - - try { - logger.debug("Initializing new RandomClient instance"); - const newClient = await (async () => { - return ((await RandomClient.defaultBuilder())) - .withWallet(JSON.parse(process.env.WALLET_JSON!)) - .withAOConfig({ - CU_URL: "https://ur-cu.randao.net", - MU_URL: "https://ur-mu.randao.net", - MODE: "legacy" - }) - .build(); - })(); - - randomClientInstance = newClient; - lastInitTime = Date.now(); - logger.debug("RandomClient initialized successfully"); - - // Clean up previous instance if it exists - if (previousInstance) { - try { - // Check if disconnect method exists before calling it - if (typeof (previousInstance as any).disconnect === 'function') { - await (previousInstance as any).disconnect().catch((e: Error) => - logger.warn('Error disconnecting previous client:', e) - ); - } - } catch (e) { - logger.warn('Error during previous client cleanup:', e as Error); - } - } - - return newClient; - } catch (error) { - // If we failed to create a new client but had a previous one, keep using it - if (previousInstance) { - logger.error('Failed to create new RandomClient, falling back to previous instance', error); - randomClientInstance = previousInstance; - return previousInstance; - } - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - logger.error('Failed to create RandomClient and no fallback available', errorMessage); - throw new Error(`Failed to initialize RandomClient: ${errorMessage}`); - } -} - -// Step 2: Process Challenge Requests (Database selection & assigning is atomic) -export async function processChallengeRequests( - client: Client, - activeChallengeRequests: { request_ids: string[] } | undefined, - parentLogId: string -): Promise { - logger.info(`${parentLogId} Step 2: Processing challenge requests.`); - - if (!activeChallengeRequests || activeChallengeRequests.request_ids.length === 0) { - logger.info(`${parentLogId} No Challenge Requests to process.`); - return; - } - - const requestIds = activeChallengeRequests.request_ids; - logger.info(`${parentLogId} Processing up to ${requestIds.length} requests.`); - - try { - await client.query('BEGIN'); // Start transaction - - logger.debug(`${parentLogId} Fetching existing request mappings.`); - - // Fetch already assigned request_id -> dbId mappings - const existingMappingsRes = await client.query( - `SELECT request_id FROM time_lock_puzzles - WHERE request_id = ANY($1) - FOR UPDATE SKIP LOCKED`, - [requestIds] - ); - - const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); - logger.debug(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); - - // Find only the unmapped requests (requestIds not in existingRequestIds) - const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); - logger.debug(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); - - let mappedEntries: { requestId: string, dbId: number }[] = []; - - if (unmappedRequestIds.length > 0) { - logger.debug(`${parentLogId} Fetching available DB entries.`); - const dbRes = await client.query( - `SELECT id FROM time_lock_puzzles - WHERE request_id IS NULL - ORDER BY id ASC - LIMIT $1 - FOR UPDATE SKIP LOCKED`, - [unmappedRequestIds.length] - ); - - const availableDbEntries = dbRes.rows.map(row => row.id); - logger.debug(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); - - if (availableDbEntries.length > 0) { - const numMappings = Math.min(unmappedRequestIds.length, availableDbEntries.length); - - for (let i = 0; i < numMappings; i++) { - await client.query( - `UPDATE time_lock_puzzles - SET request_id = $1 - WHERE id = $2`, - [unmappedRequestIds[i], availableDbEntries[i]] - ); - mappedEntries.push({ requestId: unmappedRequestIds[i], dbId: availableDbEntries[i] }); - logger.debug(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); - } - } else { - logger.warn(`${parentLogId} No available DB entries for unmapped requests.`); - } - } - - // Collect all request IDs (previously mapped + newly mapped) - const allRequestIds = [...existingRequestIds, ...mappedEntries.map(entry => entry.requestId)]; - - if (allRequestIds.length === 0) { - logger.info(`${parentLogId} No requests to process. Committing transaction.`); - await client.query('COMMIT'); - return; - } - - await client.query('COMMIT'); // Commit all updates at once - logger.info(`${parentLogId} Committed all changes. Now fulfilling challenges.`); - - // Call fulfillRandomChallenge for all request IDs - await Promise.all( - allRequestIds.map(requestId => - fulfillRandomChallenge(client, requestId, parentLogId) - .catch(error => logger.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) - ) - ); - - logger.info(`${parentLogId} All challenges fulfilled`); - } catch (error: any) { - logger.error(`${parentLogId} Error in processChallengeRequests:`, error); - await client.query('ROLLBACK'); // Rollback on failure - - logger.error(`SQL State: ${error.code}, Message: ${error.message}`); - } -} - -// Step 3: Process Output Requests -export async function processOutputRequests( - client: Client, - activeOutputRequests: { request_ids: string[] } | undefined, - parentLogId: string -): Promise { - logger.info(`${parentLogId} Step 3: Processing output requests.`); - - if (!activeOutputRequests || activeOutputRequests.request_ids.length === 0) { - logger.info(`${parentLogId} No Output Requests to process.`); - return; - } - - const outputPromises = activeOutputRequests.request_ids.map(async (requestId) => { - logger.debug(`${parentLogId} Processing output request ID: ${requestId}`); - - // Run fulfillRandomOutput asynchronously (do not await) - fulfillRandomOutput(client, requestId, parentLogId) - .catch(error => logger.error(`${parentLogId} Error fulfilling output:`, error)); - }); - - await Promise.all(outputPromises); - logger.info(`${parentLogId} Step 3 completed.`); -} - -// Step 4: Remove fulfilled entries no longer in use -export async function cleanupFulfilledEntries( - client: Client, - openRequests: any, - parentLogId: string -): Promise { - logger.info(`${parentLogId} Step 4: Checking for fulfilled entries no longer in use.`); - - const now = new Date(); - const cutoffTime = new Date(now.getTime() - COMPLETION_RETENTION_PERIOD_MS); - - try { - await client.query('BEGIN'); - - // Fetch all entries with a request_id - const result = await client.query(` - SELECT id, request_id, detected_completed - FROM time_lock_puzzles - WHERE request_id IS NOT NULL - `); - - let markForDeletion: string[] = []; - let markAsCompleted: string[] = []; - - for (const row of result.rows) { - const { id, request_id, detected_completed } = row; - - // Check if this request is still active in challenge or output - const isStillInChallenge = openRequests.activeChallengeRequests?.request_ids.includes(request_id); - const isStillInOutput = openRequests.activeOutputRequests?.request_ids.includes(request_id); - - if (!isStillInChallenge && !isStillInOutput) { - if (!detected_completed) { - // Mark it for deletion by setting detected_completed timestamp - markAsCompleted.push(id); - } else if (new Date(detected_completed) < cutoffTime) { - // If already marked and older than retention period, delete it - markForDeletion.push(id); - } - } - } - - // Mark entries as completed - if (markAsCompleted.length > 0) { - await client.query(` - UPDATE time_lock_puzzles - SET detected_completed = NOW() - WHERE id = ANY($1) - `, [markAsCompleted]); - logger.debug(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); - } - - // Delete old completed entries - if (markForDeletion.length > 0) { - await client.query(` - DELETE FROM rsa_keys - WHERE id IN ( - SELECT rsa_id FROM time_lock_puzzles WHERE id = ANY($1) - ); - `, [markForDeletion]); - - await client.query(` - DELETE FROM time_lock_puzzles - WHERE id = ANY($1); - `, [markForDeletion]); - - logger.info(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); - } - - await client.query('COMMIT'); - } catch (error) { - logger.error(`${parentLogId} Error in cleanupFulfilledEntries:`, error); - await client.query('ROLLBACK'); - } - - logger.info(`${parentLogId} Step 4 completed.`); -} - - -/** - * Function to log request timestamps - * Adds new request IDs to the tracking map and removes ones that are no longer present - * @param allRequestIds Array of request IDs to track - */ -export function logRequestTimestamps(allRequestIds: string[]): void { - const currentTime = Date.now(); - const existingIds = new Set(requestTimestamps.keys()); - - // Add new request IDs with current timestamp - for (const requestId of allRequestIds) { - if (!requestTimestamps.has(requestId)) { - logger.verbose(`Adding new request ID to tracking: ${requestId}`); - requestTimestamps.set(requestId, currentTime); - } - } - - // Remove request IDs that are no longer present - for (const existingId of existingIds) { - if (!allRequestIds.includes(existingId)) { - logger.verbose(`Removing request ID from tracking: ${existingId}`); - requestTimestamps.delete(existingId); - } - } -} - -// Function to check for defunct requests and crank if needed -export async function crank() { - const currentTime = Date.now(); - const defunctRequestIds: string[] = []; - const DEFUNCT_THRESHOLD_MS = 30 * 1000; // 30 seconds - - // Check for defunct request IDs (those that have been in the map for over 30 seconds) - requestTimestamps.forEach((timestamp, requestId) => { - const timeInMap = currentTime - timestamp; - if (timeInMap > DEFUNCT_THRESHOLD_MS) { - defunctRequestIds.push(requestId); - logger.info(`Defunct request found: ${requestId} (in system for ${Math.floor(timeInMap / 1000)} seconds)`); - } - }); - - //If there are any defunct requests, run the crank -// if (defunctRequestIds.length > 0) { -// logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); -// (await getRandomClient()).crank(); -// } else { -// // 1 in 100 chance to crank -// if (Math.floor(Math.random() * 100) === 0) { -// logger.info("Cranking randomly (1 in 100 chance hit)"); -// //(await getRandomClient()).crank(); -// } -// } -} - -export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { - const defaultResponse: GetOpenRandomRequestsResponse = { - providerId: PROVIDER_ID, - activeChallengeRequests: { request_ids: [] }, - activeOutputRequests: { request_ids: [] } - }; - - let client: RandomClient | null = null; - let response; - - try { - // Get client with error handling - try { - client = await getRandomClient(); - if (!client) { - throw new Error('Failed to initialize RandomClient'); - } - } catch (clientError) { - logger.error(`${parentLogId} Failed to initialize RandomClient:`, clientError); - return defaultResponse; - } - - // Try to get provider activity with retry logic - const maxRetries = 2; - let lastError: Error | null = null; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - response = await client.getAllProviderActivity(); - lastError = null; - break; // Success, exit retry loop - } catch (error) { - lastError = error as Error; - logger.warn(`${parentLogId} Attempt ${attempt}/${maxRetries} failed to fetch provider activity:`, error); - - if (attempt < maxRetries) { - // Only recreate the client if this isn't the last attempt - try { - randomClientInstance = null; // Force client recreation on next attempt - client = await getRandomClient(); - logger.info(`${parentLogId} Recreated RandomClient for retry attempt ${attempt + 1}`); - } catch (retryError) { - logger.error(`${parentLogId} Failed to recreate RandomClient for retry:`, retryError); - } - // Wait before retrying - await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); - } - } - } - - // If we still have an error after retries, handle it - if (lastError) { - throw lastError; - } - - if (!response) { - throw new Error('No response from provider activity'); - } - - // Collect all request IDs from all providers for tracking - const allRequestIds: string[] = []; - - // Process each provider to extract request IDs - for (const provider of response) { - try { - // Extract challenge request IDs - if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { - try { - const parsedChallengeData = JSON.parse(provider.active_challenge_requests); - if (parsedChallengeData && typeof parsedChallengeData === 'object' && 'request_ids' in parsedChallengeData) { - const requestIds = parsedChallengeData.request_ids; - if (Array.isArray(requestIds)) { - for (const id of requestIds) { - if (typeof id === 'string') { - allRequestIds.push(id); - } - } - } - } - } catch (parseErr) { - logger.warn(`${parentLogId} Warning: Failed to parse challenge requests JSON for provider ${provider.provider_id}:`, parseErr); - } - } - - // Extract output request IDs - if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { - try { - const parsedOutputData = JSON.parse(provider.active_output_requests); - if (parsedOutputData && typeof parsedOutputData === 'object' && 'request_ids' in parsedOutputData) { - const requestIds = parsedOutputData.request_ids; - if (Array.isArray(requestIds)) { - for (const id of requestIds) { - if (typeof id === 'string') { - allRequestIds.push(id); - } - } - } - } - } catch (parseErr) { - logger.warn(`${parentLogId} Warning: Failed to parse output requests JSON for provider ${provider.provider_id}:`, parseErr); - } - } - } catch (err) { - logger.warn(`${parentLogId} Warning: Failed to process provider ${provider?.provider_id || 'unknown'}:`, err); - } - } - - // Log all the request IDs for tracking - logger.debug(`${parentLogId} Found ${allRequestIds.length} request IDs across all providers`); - - // Update the request timestamps tracking - logRequestTimestamps(allRequestIds); - - const provider = response.find((p: any) => p.provider_id === PROVIDER_ID); - - if (!provider) { - logger.warn(`${parentLogId} Warning: Provider with ID ${PROVIDER_ID} not found.`); - return defaultResponse; - } - - // Attempt to parse fields if they exist, otherwise default to empty arrays - let parsedChallengeRequests: RequestList = { request_ids: [] }; - let parsedOutputRequests: RequestList = { request_ids: [] }; - - try { - if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { - const parsed = JSON.parse(provider.active_challenge_requests); - if (parsed && typeof parsed === 'object' && 'request_ids' in parsed) { - // Ensure we only include string values in the request_ids array - const validRequestIds = Array.isArray(parsed.request_ids) - ? parsed.request_ids.filter((id: any) => typeof id === 'string') - : []; - parsedChallengeRequests = { request_ids: validRequestIds }; - } - } - } catch (err) { - logger.warn(`${parentLogId} Warning: Failed to parse active_challenge_requests:`, err); - } - - try { - if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { - const parsed = JSON.parse(provider.active_output_requests); - if (parsed && typeof parsed === 'object' && 'request_ids' in parsed) { - // Ensure we only include string values in the request_ids array - const validRequestIds = Array.isArray(parsed.request_ids) - ? parsed.request_ids.filter((id: any) => typeof id === 'string') - : []; - parsedOutputRequests = { request_ids: validRequestIds }; - } - } - } catch (err) { - logger.warn(`${parentLogId} Warning: Failed to parse active_output_requests:`, err); - } - - // Only update current_onchain_random if successful - if (typeof provider.random_balance === 'number') { - current_onchain_random = provider.random_balance; - } - - const result: GetOpenRandomRequestsResponse = { - providerId: provider.provider_id || PROVIDER_ID, - activeChallengeRequests: parsedChallengeRequests, - activeOutputRequests: parsedOutputRequests, - }; - - logger.verbose(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(result)}`); - logger.info(`${parentLogId} Step 1: Open Challenge Requests count: ${result.activeChallengeRequests.request_ids.length}`); - logger.info(`${parentLogId} Step 1: Open Output Requests count: ${result.activeOutputRequests.request_ids.length}`); - - return result; - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`${parentLogId} Error in getProviderRequests:`, errorMessage); - logger.debug(`${parentLogId} Error details:`, error); - - // If we have a client that might be in a bad state, try to clean it up - if (client) { - try { - randomClientInstance = null; - } catch (cleanupError) { - logger.warn(`${parentLogId} Error during client cleanup:`, cleanupError); - } - } - - return defaultResponse; - } -} - -// Function to check and fetch database entries as needed -export async function checkAndFetchIfNeeded(client: Client) { - try { - // Query current count of usable DB entries - const res = await client.query( - 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' - ); - const currentCount = parseInt(res.rows[0].count, 10); - logger.info(`Total usable DB entries: ${currentCount}`); - - switch (current_onchain_random) { - case -1: - logger.warn("Value is -1"); - logger.warn("Provider has been shut down by USER..."); - logger.warn("Go to the provider dashboard to turn back on"); - await updateAvailableValuesAsync(-1); - break; - case -2: - logger.error("Value is -2"); - logger.error("Provider has been shut down by PROCESS..."); - logger.error("This is due to One of the following: "); - logger.error("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); - logger.error("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)"); - logger.error("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); - logger.error("Go to the provider dashboard to turn back on"); - await updateAvailableValuesAsync(-2); - break; - case -3: - logger.warn("Value is -3"); - logger.warn("Provider has been shut down by PROCESS..."); - logger.warn("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); - logger.warn("Go to the provider dashboard to turn back on"); - await updateAvailableValuesAsync(-3); - break; - case -10: - logger.info("Value is -10"); - logger.info("Provider has been turned on and is starting up OR is not staked yet"); - logger.info("Go to the provider dashboard to Stake if you have not yet OR wait for provider to finish turning on if you have staked already"); - break; - default: - logger.debug("Provider is up and working"); - logger.debug(`Onchain Value is ${current_onchain_random}`); - logger.debug(`Local Value is ${currentCount}`); - await updateAvailableValuesAsync(currentCount); - - } - - // First check if a random generation is already in progress. If so, see if it's done - if (ongoingRandomGeneration) { - logger.info('A random generation process is already running. Skipping new request.'); - await monitorDockerContainers(); - return; - } - - // Check if more entries are needed - const entriesNeeded = MINIMUM_ENTRIES - currentCount; - if (entriesNeeded < MINIMUM_RANDOM_DELTA) return; - - // Set the flag to prevent concurrent random generation - ongoingRandomGeneration = true; - - // Limit to MAX_RANDOM_PER_REQUEST - const randomToGenerate = Math.min(entriesNeeded, MAX_RANDOM_PER_REQUEST); - - logger.info(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${randomToGenerate} entries (out of ${entriesNeeded} needed)...`); - - // Start the random generation with the calculated amount - await getMoreRandom(randomToGenerate); - - } catch (error) { - logger.error('Error during check and fetch:', error); - ongoingRandomGeneration = false; // Reset the flag on error - } -} - -export async function updateAvailableValuesAsync(currentCount: number) { - const now = Date.now(); - - if (now - lastUpdatedOnChainTime < FIFTEEN_MINUTES_MS) { - logger.info(`On-chain update skipped - only ${Math.floor((now - lastUpdatedOnChainTime) / 1000)}s since last update`); - return; - } - - try { - const monitoringData = await monitoring.getMonitoringData(); - - await (await getRandomClient()).updateProviderAvailableValues(currentCount, monitoringData); - logger.info(`Updated provider values to ${currentCount}`); - - lastUpdatedOnChainTime = now; - } catch (error) { - logger.error("Failed to update provider values:", error); - monitoring.incrementErrorCount(); - } -} - -// Function to post VDF challenge (fetches dbId dynamically) -async function fulfillRandomChallenge(client: Client, requestId: string, parentLogId: string): Promise { - const logPrefix = `${parentLogId} [Challenge ${requestId}]`; - - // Check if this request is already being processed - if (challengeCooldowns.has(requestId)) { - logger.debug(`${logPrefix} Request is in cooldown, skipping...`); - return; - } - - // Set cooldown immediately to prevent concurrent processing - challengeCooldowns.set(requestId, true); - const cooldownTimer = setTimeout(() => { - challengeCooldowns.delete(requestId); - logger.debug(`${logPrefix} Cooldown released`); - }, 60000); // 1 minute cooldown - - let randomClient: RandomClient | null = null; - let retryCount = 0; - const maxRetries = 3; - const baseDelay = 1000; // 1 second base delay - - while (retryCount <= maxRetries) { - try { - // Get a fresh client for each attempt - randomClient = await getRandomClient(); - if (!randomClient) { - throw new Error('Failed to initialize RandomClient'); - } - - // Fetch the necessary details from the database using requestId - const res = await client.query( - `SELECT id, modulus, x - FROM time_lock_puzzles - WHERE request_id = $1`, - [requestId] - ); - - if (!res.rowCount) { - logger.error(`${logPrefix} No database entry found for request`); - return; - } - - - const { id: dbId, modulus, x: input } = res.rows[0]; - - if (!modulus || !input) { - throw new Error('Missing required fields in database entry'); - } - - logger.debug(`${logPrefix} Posting VDF challenge for DB ID: ${dbId}`); - - // Post the VDF challenge - await randomClient.commit({ - requestId: requestId, - puzzle: { - input: input, - modulus: modulus - } - }); - - logger.info(`${logPrefix} Successfully posted VDF challenge`); - return; // Success, exit the function - - } catch (error) { - retryCount++; - const errorMessage = error instanceof Error ? error.message : String(error); - - if (retryCount <= maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff - logger.warn(`${logPrefix} Attempt ${retryCount}/${maxRetries} failed, retrying in ${delay}ms:`, errorMessage); - - // Force client recreation on retry - randomClient = null; - randomClientInstance = null; - - await new Promise(resolve => setTimeout(resolve, delay)); - } else { - logger.error(`${logPrefix} All ${maxRetries} attempts failed:`, error); - // The cooldown was already set at the beginning - return; - } - } finally { - // Ensure the client is properly cleaned up - if (randomClient) { - try { - if (typeof (randomClient as any).disconnect === 'function') { - await (randomClient as any).disconnect().catch((e: Error) => - logger.warn(`${logPrefix} Error disconnecting client:`, e) - ); - } - } catch (e) { - logger.warn(`${logPrefix} Error during client cleanup:`, e); - } - } - } - } -} - -// Function to post VDF output and proof -async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { - const logPrefix = `${parentLogId} [Output ${requestId}]`; - - // Check if this request is already being processed - if (outputCooldowns.has(requestId)) { - logger.debug(`${logPrefix} Request is in cooldown, skipping...`); - return; - } - - // Set cooldown immediately to prevent concurrent processing - outputCooldowns.set(requestId, true); - const cooldownTimer = setTimeout(() => { - outputCooldowns.delete(requestId); - logger.debug(`${logPrefix} Cooldown released`); - }, 60000); // 1 minute cooldown - - let randomClient: RandomClient | null = null; - let retryCount = 0; - const maxRetries = 3; - const baseDelay = 1000; // 1 second base delay - - while (retryCount <= maxRetries) { - try { - // Get a fresh client for each attempt - randomClient = await getRandomClient(); - if (!randomClient) { - throw new Error('Failed to initialize RandomClient'); - } - - // Fetch the output and proof from the database using the requestId - const res = await client.query( - `SELECT - tlp.id, - tlp.y AS output, - rk.p, - rk.q - FROM time_lock_puzzles tlp - JOIN rsa_keys rk ON tlp.rsa_id = rk.id - WHERE tlp.request_id = $1`, - [requestId] - ); - - if (!res.rowCount) { - logger.error(`${logPrefix} No database entry found for request`); - return; - } - - - const { id: dbId, output, p: rsaP, q: rsaQ } = res.rows[0]; - - if (!output || !rsaP || !rsaQ) { - throw new Error('Missing required fields in database entry'); - } - - logger.debug(`${logPrefix} Posting VDF output and proof for DB ID: ${dbId}`); - - // Post the VDF output and proof - await randomClient.reveal({ - requestId: requestId, - rsa_key: { - p: rsaP, - q: rsaQ - } - }); - - logger.info(`${logPrefix} Successfully posted VDF output and proof`); - return; // Success, exit the function - - } catch (error) { - retryCount++; - const errorMessage = error instanceof Error ? error.message : String(error); - - if (retryCount <= maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff - logger.warn(`${logPrefix} Attempt ${retryCount}/${maxRetries} failed, retrying in ${delay}ms:`, errorMessage); - - // Force client recreation on retry - randomClient = null; - randomClientInstance = null; - - await new Promise(resolve => setTimeout(resolve, delay)); - } else { - logger.error(`${logPrefix} All ${maxRetries} attempts failed:`, error); - // The cooldown was already set at the beginning - return; - } - } finally { - // Ensure the client is properly cleaned up - if (randomClient) { - try { - if (typeof (randomClient as any).disconnect === 'function') { - await (randomClient as any).disconnect().catch((e: Error) => - logger.warn(`${logPrefix} Error disconnecting client:`, e) - ); - } - } catch (e) { - logger.warn(`${logPrefix} Error during client cleanup:`, e); - } - } - } - } -} - -export async function shutdown() { - const logPrefix = '[Shutdown]'; - let randomClient: RandomClient | null = null; - - try { - logger.info(`${logPrefix} Starting graceful shutdown sequence`); - - // Get monitoring data for final update - const monitoringData = await monitoring.getMonitoringData(); - - // Get a fresh client for the shutdown sequence - randomClient = await getRandomClient(); - if (!randomClient) { - throw new Error('Failed to initialize RandomClient during shutdown'); - } - - // Set provider available values to 0 and include final monitoring data - logger.info(`${logPrefix} Updating provider values to 0...`); - await randomClient.updateProviderAvailableValues(0, monitoringData); - - logger.info(`${logPrefix} Provider values updated to 0`); - - // Add a small delay to ensure the update is processed - await new Promise(resolve => setTimeout(resolve, 2000)); - - logger.info(`${logPrefix} Shutdown sequence completed`); - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`${logPrefix} Error during shutdown:`, errorMessage); - logger.debug(`${logPrefix} Error details:`, error); - monitoring.incrementErrorCount(); - } finally { - // Ensure the client is properly cleaned up - if (randomClient) { - try { - if (typeof (randomClient as any).disconnect === 'function') { - await (randomClient as any).disconnect().catch((e: Error) => - logger.warn(`${logPrefix} Error disconnecting client:`, e) - ); - } - } catch (e) { - logger.warn(`${logPrefix} Error during client cleanup:`, e); - } - } - - // Clear the client instance to ensure a fresh start if the process continues - randomClientInstance = null; - } -} diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 0e4a0b2..217dd77 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -8,7 +8,7 @@ import { setTimeout, setInterval } from 'timers'; let randomClientInstance: RandomClient | null = null; let lastInitTime: number = 0; -const REINIT_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds +const REINIT_INTERVAL = 60 * 1 * 1000; // 1 minute in milliseconds let current_onchain_random = - 10 // Cooldown tracking for updateAvailableValuesAsync @@ -25,85 +25,65 @@ const MAX_RANDOM_PER_REQUEST = 500; // Maximum number of random values to genera // Map to track request timestamps const requestTimestamps: Map = new Map(); +let isInitializing = false; // Track if initialization is in progress +let initPromise: Promise | null = null; // Store the initialization promise + // Function to reset the ongoingRandomGeneration flag export function resetOngoingRandomGeneration() { ongoingRandomGeneration = false; logger.info('Random generation flag reset. System ready for new random generation requests.'); } -// Optional: Auto-reinitialize on a timer -setInterval(() => { - randomClientInstance = null; -}, REINIT_INTERVAL); - export async function getRandomClient(): Promise { const currentTime = Date.now(); - // If we have a valid instance and it's not time to recreate, return it + // Return the existing instance if it's valid and fresh if (randomClientInstance && (currentTime - lastInitTime) <= REINIT_INTERVAL) { return randomClientInstance; } - // If we're already in the process of creating a new instance, wait for it - if (randomClientInstance === null && lastInitTime > 0) { - const waitStart = Date.now(); - while (Date.now() - waitStart < 30000) { // 30 second timeout - await new Promise(resolve => setTimeout(resolve, 100)); - if (randomClientInstance) { - return randomClientInstance; - } - } - throw new Error('Timeout waiting for RandomClient initialization'); + // If reinitialization is already happening, serve the old instance + if (isInitializing) { + logger.info('[RandomClient] Reinitialization in progress, serving old instance'); + return randomClientInstance!; } - // Mark that we're creating a new instance - const previousInstance = randomClientInstance; - randomClientInstance = null; - lastInitTime = 0; + // Start reinitialization in the background + isInitializing = true; + logger.info('[RandomClient] Background reinitialization triggered'); - try { - logger.debug("Initializing new RandomClient instance"); - const newClient = await (async () => { - return ((await RandomClient.defaultBuilder())) + (async () => { + try { + const newClient = await (await RandomClient.defaultBuilder()) .withWallet(JSON.parse(process.env.WALLET_JSON!)) .withAOConfig({ - CU_URL: "https://ur-cu.randao.net", - MU_URL: "https://ur-mu.randao.net", - MODE: "legacy" + CU_URL: process.env.CU_URL || "https://ur-cu.randao.net", + MU_URL: process.env.MU_URL || "https://ur-mu.randao.net", + MODE: "legacy" as const }) .build(); - })(); - randomClientInstance = newClient; - lastInitTime = Date.now(); - logger.debug("RandomClient initialized successfully"); - - // Clean up previous instance if it exists - if (previousInstance) { - try { - // Check if disconnect method exists before calling it - if (typeof (previousInstance as any).disconnect === 'function') { - await (previousInstance as any).disconnect().catch((e: Error) => - logger.warn('Error disconnecting previous client:', e) - ); - } - } catch (e) { - logger.warn('Error during previous client cleanup:', e as Error); - } + // Swap instance only once it's ready + randomClientInstance = newClient; + lastInitTime = Date.now(); + logger.info('[RandomClient] Successfully reinitialized client'); + } catch (err) { + logger.error('[RandomClient] Reinitialization failed:', err); + } finally { + isInitializing = false; } + })(); - return newClient; - } catch (error) { - // If we failed to create a new client but had a previous one, keep using it - if (previousInstance) { - logger.error('Failed to create new RandomClient, falling back to previous instance', error); - randomClientInstance = previousInstance; - return previousInstance; - } - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - logger.error('Failed to create RandomClient and no fallback available', errorMessage); - throw new Error(`Failed to initialize RandomClient: ${errorMessage}`); - } + // Return the old instance immediately + return randomClientInstance!; +} + + +// Add a method to explicitly refresh the client if needed +export async function refreshRandomClient(): Promise { + randomClientInstance = null; + lastInitTime = 0; + return getRandomClient(); } // Step 2: Process Challenge Requests (Database selection & assigning is atomic) @@ -351,16 +331,16 @@ export async function crank() { }); // If there are any defunct requests, run the crank -// if (defunctRequestIds.length > 0) { -// logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); -// (await getRandomClient()).crank(); -// } else { -// // 1 in 100 chance to crank -// if (Math.floor(Math.random() * 100) === 0) { -// logger.info("Cranking randomly (1 in 100 chance hit)"); -// //(await getRandomClient()).crank(); -// } -// } +if (defunctRequestIds.length > 0) { + logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); + (await getRandomClient()).crank(); +} else { + // 1 in 100 chance to crank + if (Math.floor(Math.random() * 100) === 0) { + logger.info("Cranking randomly (1 in 100 chance hit)"); + //(await getRandomClient()).crank(); + } +} } export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { diff --git a/requester/src/app.ts b/requester/src/app.ts index 15e1ac4..5b4f890 100644 --- a/requester/src/app.ts +++ b/requester/src/app.ts @@ -3,7 +3,7 @@ import { } from "ao-process-clients"; //import { TransferToProviders } from "./extra"; -const RETRY_DELAY_MS = 1000; // 1 seconds +const RETRY_DELAY_MS = 5000; // 1 seconds const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute const CHANCE_TO_CALL_RANDOM = 1; @@ -22,7 +22,7 @@ let lastProviderRefresh = 0; let randomClientInstance: RandomClient | null = null; -async function getRandomClient(): Promise { +export async function getRandomClient(): Promise { if (!randomClientInstance) { randomClientInstance = ((await RandomClient.defaultBuilder())) @@ -120,11 +120,19 @@ async function getRandomProviders(randclient: RandomClient): Promise<{ providers } } +import { startRequestTracker } from "./requestTracker"; + async function main() { const randclient = await getRandomClient() //const stakeclient = ProviderStakingClient.autoConfiguration(); randclient.prepay(1000_000000000) //1,000 + + // Start request tracker in a separate process + // This will continuously poll for provider activity and crank defunct requests + startRequestTracker().catch(error => { + console.error("Error starting request tracker:", error); + }); while (true) { console.log("Running") try { diff --git a/requester/src/extra.ts b/requester/src/extra.ts deleted file mode 100644 index fb6805c..0000000 --- a/requester/src/extra.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { connect, createDataItemSigner } from "@permaweb/aoconnect"; - -const { spawn, message, result } = connect({ - MU_URL: "https://ur-mu.randao.net", - CU_URL: "https://ur-cu.randao.net", - // MU_URL: "https://mu.ao-testnet.xyz", - // CU_URL: "https://cu.ao-testnet.xyz", - GATEWAY_URL: "https://arweave.net", - MODE: "legacy" - -}); -const TOKEN_PROCESS = "rPpsRk9Rm8_SJ1JF8m9_zjTalkv9Soaa_5U0tYUloeY" -const RAND_PROCESS = "8N08BvmC34q9Hxj-YS6eAOd_cSmYqGpezPPHUYWJBhg" -export async function fetchMessageResult( - messageID: string, - processID: string -): Promise<{ Messages: any[]; Spawns: any[]; Output: any[]; Error: any }> { - try { - const response = await result({ - message: messageID, - process: processID, - }); - - return { - Messages: response.Messages || [], - Spawns: response.Spawns || [], - Output: response.Output || [], - Error: response.Error || null - }; - } catch (error: any) { - if (error instanceof SyntaxError && error.message.includes("Unexpected token '<'")) { - console.error("CU timeout ratelimit error"); - await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 60 seconds - } else { - console.error("Error fetching message result:", error); - } - - return { Messages: [], Spawns: [], Output: [], Error: error }; - } -} -export async function TransferToProviders(providerIds: string[], callbackID:string) { - try { - const sentMessage = await message({ - process: TOKEN_PROCESS, - tags: [ - { name: "Action", value: "Transfer" }, - { name: "library", value: "npm install ao-process-clients" }, - { name: "Quantity", value: "100" }, - { name: "Recipient", value: RAND_PROCESS }, - { name: "X-CallbackId", value: callbackID }, - { name: "X-Providers", value: JSON.stringify({ provider_ids: providerIds }) }, - { name: "X-RequestedInputs", value: JSON.stringify({ requested_inputs: providerIds.length }) }, - ], - signer: createDataItemSigner(JSON.parse(process.env.REQUEST_WALLET_JSON!)), - data: "", - }); - - const result = await fetchMessageResult(sentMessage,TOKEN_PROCESS); - return result; - } catch (error) { - console.error("Transfer message failed:", error); - throw error; - } -} \ No newline at end of file diff --git a/requester/src/requestTracker.ts b/requester/src/requestTracker.ts new file mode 100644 index 0000000..b0dae03 --- /dev/null +++ b/requester/src/requestTracker.ts @@ -0,0 +1,171 @@ +import { RandomClient } from "ao-process-clients"; +import { getRandomClient } from "./app"; + +// Map to track request timestamps +const requestTimestamps: Map = new Map(); +const DEFUNCT_THRESHOLD_MS = 30 * 1000; // 30 seconds +const POLL_INTERVAL_MS = 1000; // Poll every second + +/** + * Function to log request timestamps + * Adds new request IDs to the tracking map and removes ones that are no longer present + * @param allRequestIds Array of request IDs to track + */ +function logRequestTimestamps(allRequestIds: string[]): void { + const currentTime = Date.now(); + const existingIds = new Set(requestTimestamps.keys()); + + // Add new request IDs with current timestamp + for (const requestId of allRequestIds) { + if (!requestTimestamps.has(requestId)) { + console.log(`Adding new request ID to tracking: ${requestId}`); + requestTimestamps.set(requestId, currentTime); + } + } + + // Remove request IDs that are no longer present + for (const existingId of existingIds) { + if (!allRequestIds.includes(existingId)) { + console.log(`Removing request ID from tracking: ${existingId}`); + requestTimestamps.delete(existingId); + } + } +} + +/** + * Check for defunct requests and crank if needed + */ +async function crankDefunctRequests(randclient: RandomClient) { + const currentTime = Date.now(); + const defunctRequestIds: string[] = []; + + // Check for defunct request IDs (those that have been in the map for over 30 seconds) + requestTimestamps.forEach((timestamp, requestId) => { + const timeInMap = currentTime - timestamp; + if (timeInMap > DEFUNCT_THRESHOLD_MS) { + defunctRequestIds.push(requestId); + console.log(`Defunct request found: ${requestId} (in system for ${Math.floor(timeInMap / 1000)} seconds)`); + } + }); + + // If there are any defunct requests, run the crank + if (defunctRequestIds.length > 0) { + console.log(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); + await randclient.crank(); + } +} + +/** + * Parse provider activity to extract all request IDs + * @param providerActivity Provider activity data from getAllProviderActivity + * @returns Array of request IDs + */ +function extractRequestIdsFromProviderActivity(providerActivity: any[]): string[] { + const allRequestIds: string[] = []; + + // Process each provider to extract request IDs + for (const provider of providerActivity) { + try { + // Extract challenge request IDs + if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { + try { + const parsedChallengeData = JSON.parse(provider.active_challenge_requests); + if (parsedChallengeData && typeof parsedChallengeData === 'object' && 'request_ids' in parsedChallengeData) { + const requestIds = parsedChallengeData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + console.warn(`Warning: Failed to parse challenge requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + + // Extract output request IDs + if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { + try { + const parsedOutputData = JSON.parse(provider.active_output_requests); + if (parsedOutputData && typeof parsedOutputData === 'object' && 'request_ids' in parsedOutputData) { + const requestIds = parsedOutputData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + console.warn(`Warning: Failed to parse output requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + } catch (err) { + console.warn(`Warning: Failed to process provider ${provider?.provider_id || 'unknown'}:`, err); + } + } + + // Remove duplicates + return [...new Set(allRequestIds)]; +} + +/** + * Main function to start tracking and cranking requests + */ +export async function startRequestTracker() { + console.log("Starting request tracker..."); + + while (true) { + try { + const randclient = await getRandomClient(); + let maxRetries = 3; + let response = null; + let lastError = null; + + // Get provider activity with retries + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + response = await randclient.getAllProviderActivity(); + lastError = null; + break; // Success, exit retry loop + } catch (error) { + lastError = error as Error; + console.warn(`Attempt ${attempt}/${maxRetries} failed to fetch provider activity:`, error); + + if (attempt < maxRetries) { + // Wait before retrying with exponential backoff + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + // If we still have an error after retries, throw it + if (lastError) { + throw lastError; + } + + if (!response) { + throw new Error('No response from provider activity'); + } + + // Extract all request IDs + const allRequestIds = extractRequestIdsFromProviderActivity(response); + console.log(`Found ${allRequestIds.length} active request IDs across all providers`); + + // Log timestamps for tracking + logRequestTimestamps(allRequestIds); + + // Check and crank defunct requests + await crankDefunctRequests(randclient); + + } catch (error) { + console.error("Error in request tracker:", error); + } + + // Wait before next polling cycle + await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)); + } +} From 449de6b8c74607704fe0b9b443de58bd9841d853 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 15 Jun 2025 11:12:24 -0400 Subject: [PATCH 69/80] v9 --- docker-compose/docker-compose.yml | 16 ++++++++++------ orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 2 +- orchestrator/src/helperFunctions.ts | 11 +++++++++++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index e2a5719..7f6c3c7 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -23,7 +23,9 @@ services: max-file: "5" orchestrator: - image: randao/orchestrator:v1.0.8 + image: randao/orchestrator:latest + pull_policy: always # Ensure the latest image is always pulled + restart: on-failure # Restart on crash (or on exit with non-zero code) depends_on: postgres: condition: service_healthy @@ -33,23 +35,25 @@ services: DB_USER: ${DB_USER:-myuser} DB_PASSWORD: ${DB_PASSWORD:-mypassword} DB_NAME: ${DB_NAME:-mydatabase} - PATH_TO_WALLET: /app/wallet.json # Path inside the container + PATH_TO_WALLET: /app/wallet.json WALLET_JSON: ${WALLET_JSON} - DOCKER_NETWORK: backend # Passing the network name + DOCKER_NETWORK: backend networks: - backend volumes: - - /var/run/docker.sock:/var/run/docker.sock # Mount Docker socket - - ./wallet.json:/app/wallet.json # Mount wallet.json into the container + - /var/run/docker.sock:/var/run/docker.sock + - ./wallet.json:/app/wallet.json logging: driver: json-file options: max-size: "100m" max-file: "5" + command: ["sh", "-c", "sleep 3600 && kill 1"] + # The above line causes the orchestrator to kill itself every hour; customize `sleep` as needed. networks: backend: - name: backend # This will set the network name explicitly + name: backend driver: bridge volumes: diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index af5b38e..b03a264 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.8 # You can change this value to any version you want +export VERSION=v1.0.9 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index d2dca95..e172fec 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -5,7 +5,7 @@ import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderReque import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; -export const VERSION = process.env.VERSION || "1.0.8"; +export const VERSION = process.env.VERSION || "1.0.9"; export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index 217dd77..01f04f6 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -566,6 +566,17 @@ export async function checkAndFetchIfNeeded(client: Client) { logger.warn("Go to the provider dashboard to turn back on"); await updateAvailableValuesAsync(-3); break; + case -4: + logger.warn("Value is -4"); + logger.warn("Nothing Set up for this yet"); + await updateAvailableValuesAsync(-4); + break; + case -5: + logger.warn("Value is -5"); + logger.warn("Provider has been told to kill itself and restart. Taking action now"); + await shutdown(); + process.exit(1); + break; case -10: logger.info("Value is -10"); logger.info("Provider has been turned on and is starting up OR is not staked yet"); From 954716c6abb344b913472872ee59851f868b7128 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 15 Jun 2025 12:17:17 -0400 Subject: [PATCH 70/80] v92 --- docker-compose/docker-compose.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 7f6c3c7..97cfac1 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -35,21 +35,17 @@ services: DB_USER: ${DB_USER:-myuser} DB_PASSWORD: ${DB_PASSWORD:-mypassword} DB_NAME: ${DB_NAME:-mydatabase} - PATH_TO_WALLET: /app/wallet.json WALLET_JSON: ${WALLET_JSON} DOCKER_NETWORK: backend networks: - backend volumes: - /var/run/docker.sock:/var/run/docker.sock - - ./wallet.json:/app/wallet.json logging: driver: json-file options: max-size: "100m" max-file: "5" - command: ["sh", "-c", "sleep 3600 && kill 1"] - # The above line causes the orchestrator to kill itself every hour; customize `sleep` as needed. networks: backend: From a5f046f7318dce2c9d8745511b0137c8befbe1dc Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 19 Jun 2025 17:45:29 -0400 Subject: [PATCH 71/80] v11 --- docker-compose/docker-compose.yml | 2 +- orchestrator/docs/development.md | 2 +- orchestrator/src/app.ts | 9 ++-- orchestrator/src/containerManagment.ts | 60 +++++++++++++++++++-- orchestrator/src/helperFunctions.ts | 74 ++++++++++++++++---------- 5 files changed, 109 insertions(+), 38 deletions(-) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 97cfac1..a7db96c 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -25,7 +25,7 @@ services: orchestrator: image: randao/orchestrator:latest pull_policy: always # Ensure the latest image is always pulled - restart: on-failure # Restart on crash (or on exit with non-zero code) + restart: unless-stopped # Restart on crash (or on exit with non-zero code) depends_on: postgres: condition: service_healthy diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index b03a264..fbb18ec 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.9 # You can change this value to any version you want +export VERSION=v1.0.11 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index e172fec..330ca06 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,20 +1,21 @@ import Docker from 'dockerode'; import { connectWithRetry, setupDatabase } from './db_tools.js'; import Arweave from 'arweave'; -import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderRequests, processChallengeRequests, processOutputRequests, shutdown } from './helperFunctions.js'; +import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderRequests, processChallengeRequests, processOutputRequests, gracefulShutdown } from './helperFunctions.js'; import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; -export const VERSION = process.env.VERSION || "1.0.9"; +export const VERSION = "1.0.10"; export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; +export const ORCHESTRATOR_IMAGE = 'randao/orchestrator:latest'; export const DOCKER_MONITORING_TIME = 30000; export const POLLING_INTERVAL_MS = 0; //0 second export const DATABASE_CHECK_TIME = 60000; //60 seconds -export const MINIMUM_ENTRIES = 5000; +export const MINIMUM_ENTRIES = 10000; export const DRYRUNTIMEOUT = 30000; // 30 seconds export const MAX_RETRIES = 10; export const RETRY_DELAY_MS = 10000; @@ -154,7 +155,7 @@ async function run(): Promise { process.on("SIGTERM", async () => { logger.info("SIGTERM received. Shutting down gracefully."); await client.end(); - await shutdown(); + await gracefulShutdown(); for (let i = 0; i < SHUTDOWN_POLLING_DELAY; i++) { try { await polling(client); diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts index b1d48bb..28588b8 100644 --- a/orchestrator/src/containerManagment.ts +++ b/orchestrator/src/containerManagment.ts @@ -9,10 +9,64 @@ export interface NetworkConfig { } // Global variables to track polling status -let pulledDockerimage = false; +// Track which images have been pulled +let pulledDockerImage = false; let pullingImagePromise: Promise | null = null; // Add at the top-level scope (module-global) const ongoingContainers = new Set(); // Track container IDs of running Docker containers +/** + * Pull a Docker image if it hasn't been pulled already + * @param imageName The name of the image to pull + * @returns A promise that resolves when the image has been pulled + */ +export async function pullDockerImage(imageName: string): Promise { + // Skip if already pulled + if (pulledDockerImage) { + logger.debug(`Image ${imageName} already pulled, skipping pull operation`); + return true; + } + + // If a pull is already in progress for this image, wait for it + if (pullingImagePromise) { + try { + await pullingImagePromise; + return true; + } catch (error) { + logger.error(`Failed to pull Docker image ${imageName}:`, error); + return false; + } + } + + // Start a new pull operation + pullingImagePromise = new Promise((resolve, reject) => { + logger.info(`Pulling image: ${imageName}`); + docker.pull(imageName, (err: Error | null, stream: NodeJS.ReadableStream | undefined) => { + if (err || !stream) { + pullingImagePromise = null; // reset on error + return false; + } + docker.modem.followProgress(stream, (doneErr: Error | null) => { + if (doneErr) { + pullingImagePromise = null; // reset on error + reject(doneErr); + } else { + pulledDockerImage = true; // Mark as pulled after success + resolve(); + } + }); + }); + }); + + try { + await pullingImagePromise; + return true; + } catch (error) { + logger.error(`Failed to pull Docker image:`, error); + pullingImagePromise = null; + return false; + } + } + export async function triggerTimePuzzleJobPod(randomCount: number): Promise { const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; @@ -22,7 +76,7 @@ export async function triggerTimePuzzleJobPod(randomCount: number): Promise((resolve, reject) => { logger.info(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); @@ -36,7 +90,7 @@ export async function triggerTimePuzzleJobPod(randomCount: number): Promise 0) { - logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); - (await getRandomClient()).crank(); -} else { - // 1 in 100 chance to crank - if (Math.floor(Math.random() * 100) === 0) { - logger.info("Cranking randomly (1 in 100 chance hit)"); - //(await getRandomClient()).crank(); - } -} +// if (defunctRequestIds.length > 0) { +// logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); +// (await getRandomClient()).crank(); +// } else { +// // 1 in 100 chance to crank +// if (Math.floor(Math.random() * 100) === 0) { +// logger.info("Cranking randomly (1 in 100 chance hit)"); +// //(await getRandomClient()).crank(); +// } +// } } export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { @@ -573,8 +573,22 @@ export async function checkAndFetchIfNeeded(client: Client) { break; case -5: logger.warn("Value is -5"); - logger.warn("Provider has been told to kill itself and restart. Taking action now"); - await shutdown(); + logger.warn("Provider has been told to pull the latest image and restart. Taking action now"); + + // Pull the randomrequester image + logger.info(`Attempting to pull Docker image: ${ORCHESTRATOR_IMAGE}`); + const pullSuccess = await pullDockerImage(ORCHESTRATOR_IMAGE); + + if (pullSuccess) { + logger.info(`Successfully pulled image: ${ORCHESTRATOR_IMAGE}`); + } else { + logger.error(`Failed to pull image: ${ORCHESTRATOR_IMAGE}`); + } + + // Regardless of pull result, proceed with shutdown and restart + await gracefulShutdown(); + const container = docker.getContainer(process.env.HOSTNAME || ""); // or use docker ps/inspect to get container ID + await container.remove({ force: true }); process.exit(1); break; case -10: @@ -842,7 +856,7 @@ async function fulfillRandomOutput(client: Client, requestId: string, parentLogI } } -export async function shutdown() { +export async function gracefulShutdown() { const logPrefix = '[Shutdown]'; let randomClient: RandomClient | null = null; @@ -875,20 +889,22 @@ export async function shutdown() { logger.debug(`${logPrefix} Error details:`, error); monitoring.incrementErrorCount(); } finally { - // Ensure the client is properly cleaned up - if (randomClient) { - try { - if (typeof (randomClient as any).disconnect === 'function') { - await (randomClient as any).disconnect().catch((e: Error) => - logger.warn(`${logPrefix} Error disconnecting client:`, e) - ); - } - } catch (e) { - logger.warn(`${logPrefix} Error during client cleanup:`, e); - } - } + // // Ensure the client is properly cleaned up + // if (randomClient) { + // try { + // if (typeof (randomClient as any).disconnect === 'function') { + // await (randomClient as any).disconnect().catch((e: Error) => + // logger.warn(`${logPrefix} Error disconnecting client:`, e) + // ); + // } + // } catch (e) { + // logger.warn(`${logPrefix} Error during client cleanup:`, e); + // } + // } + + // // Clear the client instance to ensure a fresh start if the process continues + // randomClientInstance = null; + - // Clear the client instance to ensure a fresh start if the process continues - randomClientInstance = null; } } From 357eac998b74cc76a224e6585ae245609398141a Mon Sep 17 00:00:00 2001 From: K <111819113+KennySwayzee93@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:27:08 -0400 Subject: [PATCH 72/80] reduce to 1/2 available cores --- .../EfficientTimeLockPuzzleSolver.py | 4 +++- .../time_lock_puzzle/TimeLockPuzzleFactory.py | 5 ++++- puzzle-generator/src/utils/SystemSpecs.py | 20 +++++++++++++++++++ puzzle-generator/src/utils/__init__.py | 5 +++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 puzzle-generator/src/utils/SystemSpecs.py create mode 100644 puzzle-generator/src/utils/__init__.py diff --git a/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py index 996909f..2705c61 100644 --- a/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py +++ b/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py @@ -2,6 +2,7 @@ from typing import List, Tuple from ..mpc import MPC +from ..utils.SystemSpecs import SystemSpecs from ..mpc.types import MPZ from ..rsa.RSA import RSA from .abstract.IEfficientTimeLockPuzzleSolver import IEfficientTimeLockPuzzleSolver @@ -32,7 +33,8 @@ def solve_many(puzzles: List[Tuple[RSA, ITimeLockPuzzle]]) -> List[MPZ]: Returns: List of solutions in the same order as input puzzles """ - with Pool() as pool: + num_workers = SystemSpecs.get_num_parallel_processes() + with Pool(num_workers) as pool: return pool.map(EfficientTimeLockPuzzleSolver._solve_single, puzzles) # Private Methods diff --git a/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py index 19380a2..1e05a0e 100644 --- a/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py +++ b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py @@ -2,6 +2,7 @@ import multiprocessing from src.time_lock_puzzle import TimeLockPuzzleBuilder +from ..utils.SystemSpecs import SystemSpecs from ..mpc import MPC from ..mpc.types import MPZ from ..random import Random @@ -50,8 +51,10 @@ def create_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: # Create parameters for each puzzle puzzle_params = [(self._bit_size, self._t) for _ in range(amount)] + num_workers = SystemSpecs.get_num_parallel_processes() + # Create puzzles in parallel using process pool - with multiprocessing.Pool() as pool: + with multiprocessing.Pool(num_workers) as pool: puzzles = pool.map( TimeLockPuzzleFactory._create_puzzle_parallel, puzzle_params ) diff --git a/puzzle-generator/src/utils/SystemSpecs.py b/puzzle-generator/src/utils/SystemSpecs.py new file mode 100644 index 0000000..8c4dbe4 --- /dev/null +++ b/puzzle-generator/src/utils/SystemSpecs.py @@ -0,0 +1,20 @@ +"""Utility class for system specifications and resource management.""" + +import multiprocessing + + +class SystemSpecs: + """Utility class for determining system specifications and resource allocation.""" + + @staticmethod + def get_num_parallel_processes() -> int: + """ + Calculate the optimal number of parallel processes to use. + + Returns half the number of CPU cores, with a minimum of 1. + + Returns: + int: Number of parallel processes to use + """ + parallelization_denominator = 2 # if cpu has 16 cores and parallelization denominator is 2 then this codebase will use 8 cores + return multiprocessing.cpu_count() // parallelization_denominator or 1 # default to 1 if only 1 core available diff --git a/puzzle-generator/src/utils/__init__.py b/puzzle-generator/src/utils/__init__.py new file mode 100644 index 0000000..d3d10ee --- /dev/null +++ b/puzzle-generator/src/utils/__init__.py @@ -0,0 +1,5 @@ +"""Utility modules for the puzzle generator.""" + +from .SystemSpecs import SystemSpecs + +__all__ = ["SystemSpecs"] From 2dc8798d82be5919f83441b31076109c570bffc2 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 22 Jun 2025 06:26:55 -0400 Subject: [PATCH 73/80] v12 --- orchestrator/docs/development.md | 4 ++-- orchestrator/package.json | 1 - orchestrator/src/app.ts | 4 ++-- puzzle-generator/docs/developing.md | 4 ++-- requester/package.json | 1 - 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/orchestrator/docs/development.md b/orchestrator/docs/development.md index fbb18ec..550e187 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -17,7 +17,7 @@ Random deletes itself from the db the moment its been used and not requested. It # Export version as an environment variable -export VERSION=v1.0.11 # You can change this value to any version you want +export VERSION=v1.0.12 # You can change this value to any version you want # Build the Docker image with the version tag docker build -t randao/orchestrator:latest -t randao/orchestrator:$VERSION . @@ -34,7 +34,7 @@ docker buildx create --use docker buildx inspect --bootstrap # Build the multi-platform image and push it -docker buildx build --platform linux/amd64,linux/arm64 \ +docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \ -t randao/orchestrator:latest \ -t randao/orchestrator:$VERSION \ --push . diff --git a/orchestrator/package.json b/orchestrator/package.json index 5dfd4af..5a4c494 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -3,7 +3,6 @@ "@types/dockerode": "^3.3.31", "@types/node": "^22.9.1", "@types/pg": "^8.11.10", - "serverless-offline": "^14.3.3", "typescript": "^5.6.3" }, "dependencies": { diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index 330ca06..c12d417 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -5,11 +5,11 @@ import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderReque import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; -export const VERSION = "1.0.10"; +export const VERSION = "1.0.12"; export const docker = new Docker(); export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; -export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.5'; +export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.6'; export const ORCHESTRATOR_IMAGE = 'randao/orchestrator:latest'; export const DOCKER_MONITORING_TIME = 30000; diff --git a/puzzle-generator/docs/developing.md b/puzzle-generator/docs/developing.md index 6a6291b..a96e296 100644 --- a/puzzle-generator/docs/developing.md +++ b/puzzle-generator/docs/developing.md @@ -60,7 +60,7 @@ pytest --cov=src # Set version as an environment variable -export VERSION=v0.1.5 # Change this value as needed +export VERSION=v0.1.6 # Change this value as needed # Initial build and tagging for local testing docker build -t randao/puzzle-gen:latest -t randao/puzzle-gen:$VERSION . @@ -77,7 +77,7 @@ docker buildx create --name arm-builder --use || docker buildx use arm-builder docker buildx inspect --bootstrap # Multi-platform build for ARM64 and AMD64, and push to Docker Hub -docker buildx build --platform linux/amd64,linux/arm64 \ +docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \ -t randao/puzzle-gen:latest \ -t randao/puzzle-gen:$VERSION \ --push . diff --git a/requester/package.json b/requester/package.json index 4e81048..49550a1 100644 --- a/requester/package.json +++ b/requester/package.json @@ -3,7 +3,6 @@ "@types/dockerode": "^3.3.31", "@types/node": "^22.9.1", "@types/pg": "^8.11.10", - "serverless-offline": "^14.3.3", "typescript": "^5.6.3" }, "dependencies": { From f4789c279d7dc388031b4019f9ed4539aecb0250 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 25 Jun 2025 11:41:23 -0400 Subject: [PATCH 74/80] testing 12-24 words --- docker-compose/.env.example | 1 + docker-compose/docker-compose.yml | 3 +- orchestrator/package.json | 12 +- orchestrator/src/app.ts | 9 +- orchestrator/src/helperFunctions.ts | 6 +- orchestrator/src/walletUtils.ts | 220 ++++++++++++++++++++++++++++ 6 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 orchestrator/src/walletUtils.ts diff --git a/docker-compose/.env.example b/docker-compose/.env.example index e7c3864..ece7ec9 100644 --- a/docker-compose/.env.example +++ b/docker-compose/.env.example @@ -3,6 +3,7 @@ DB_PASSWORD=mypassword DB_NAME=mydatabase DOCKER_NETWORK=backend LOG_CONSOLE_LEVEL=3 +SEED_PHRASE="Create a NEW wallet and enter the 12 - 24 words here" WALLET_JSON = '{ "kty": "RSA", "e": "test", diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index a7db96c..f46a33a 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -24,7 +24,7 @@ services: orchestrator: image: randao/orchestrator:latest - pull_policy: always # Ensure the latest image is always pulled + # pull_policy: always # Ensure the latest image is always pulled restart: unless-stopped # Restart on crash (or on exit with non-zero code) depends_on: postgres: @@ -35,6 +35,7 @@ services: DB_USER: ${DB_USER:-myuser} DB_PASSWORD: ${DB_PASSWORD:-mypassword} DB_NAME: ${DB_NAME:-mydatabase} + SEED_PHRASE: ${SEED_PHRASE} WALLET_JSON: ${WALLET_JSON} DOCKER_NETWORK: backend networks: diff --git a/orchestrator/package.json b/orchestrator/package.json index 5a4c494..ccadef7 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -1,6 +1,7 @@ { "devDependencies": { "@types/dockerode": "^3.3.31", + "@types/human-crypto-keys": "^0.1.3", "@types/node": "^22.9.1", "@types/pg": "^8.11.10", "typescript": "^5.6.3" @@ -11,9 +12,18 @@ "arweave": "^1.15.5", "aws-sdk": "^2.1692.0", "axios": "^1.7.7", + "bip39": "^3.1.0", + "bip39-web-crypto": "^4.0.1", + "bs58": "^6.0.0", + "check-password-strength": "^3.0.0", "crypto": "^1.0.1", "dockerode": "^4.0.2", - "pg": "^8.13.1" + "ed25519-hd-key": "^1.3.0", + "human-crypto-keys": "^0.1.4", + "lodash": "^4.17.21", + "pg": "^8.13.1", + "tweetnacl": "^1.0.3", + "typed-assert": "^1.0.9" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts index c12d417..d511f33 100644 --- a/orchestrator/src/app.ts +++ b/orchestrator/src/app.ts @@ -1,6 +1,7 @@ import Docker from 'dockerode'; import { connectWithRetry, setupDatabase } from './db_tools.js'; -import Arweave from 'arweave'; +import Arweave from "arweave"; +import { getWalletAddress } from "./walletUtils"; import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderRequests, processChallengeRequests, processOutputRequests, gracefulShutdown } from './helperFunctions.js'; import logger, { LogLevel, Logger } from './logger'; import { monitoring } from './monitoring'; @@ -146,9 +147,13 @@ async function run(): Promise { const client = await connectWithRetry(); await setupDatabase(client); - arweave.wallets.jwkToAddress(JSON.parse(process.env.WALLET_JSON!)).then((address) => { + // Initialize wallet and set provider ID using wallet utilities + getWalletAddress().then((address) => { logger.info(`Provider ID: ${address}`); PROVIDER_ID = address; + }).catch(error => { + logger.error('Failed to initialize wallet:', error); + process.exit(1); }); // Handle graceful shutdown before entering infinite loop diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts index f4930bb..82f0360 100644 --- a/orchestrator/src/helperFunctions.ts +++ b/orchestrator/src/helperFunctions.ts @@ -5,6 +5,7 @@ import { getMoreRandom, monitorDockerContainers, pullDockerImage } from "./conta import logger, { LogLevel } from "./logger"; import { monitoring } from "./monitoring"; import { setTimeout, setInterval } from 'timers'; +import { getWallet } from "./walletUtils"; let randomClientInstance: RandomClient | null = null; let lastInitTime: number = 0; @@ -54,8 +55,11 @@ export async function getRandomClient(): Promise { (async () => { try { + // Use the wallet utilities to get the wallet (prioritizes SEED_PHRASE over WALLET_JSON) + const wallet = await getWallet(); + const newClient = await (await RandomClient.defaultBuilder()) - .withWallet(JSON.parse(process.env.WALLET_JSON!)) + .withWallet(wallet) .withAOConfig({ CU_URL: process.env.CU_URL || "https://ur-cu.randao.net", MU_URL: process.env.MU_URL || "https://ur-mu.randao.net", diff --git a/orchestrator/src/walletUtils.ts b/orchestrator/src/walletUtils.ts new file mode 100644 index 0000000..b6f7147 --- /dev/null +++ b/orchestrator/src/walletUtils.ts @@ -0,0 +1,220 @@ +import { getKeyPairFromSeed } from "human-crypto-keys"; +import type { JWKInterface } from "arweave/web/lib/wallet"; +import { passwordStrength } from "check-password-strength"; +import { isOneOf, isString } from "typed-assert"; +import { wordlists, mnemonicToSeed } from "bip39-web-crypto"; +import logger from "./logger"; +import Arweave from "arweave"; + +// Initialize Arweave +const arweave = Arweave.init({ + host: "arweave.net", + port: 443, + protocol: "https", +}); + +// Global wallet storage +let globalWallet: JWKInterface | null = null; +let walletSource: 'seed_phrase' | 'json' | null = null; + +/** + * Credits to arweave.app for the mnemonic wallet generation + * + * https://github.com/jfbeats/ArweaveWebWallet/blob/master/src/functions/Wallets.ts + * https://github.com/jfbeats/ArweaveWebWallet/blob/master/src/functions/Crypto.ts + */ + +/** + * Generate a JWK from a mnemonic seedphrase + * + * @param mnemonic Mnemonic seedphrase to generate wallet from + * @returns Wallet JWK + */ +export async function jwkFromMnemonic(mnemonic: string): Promise { + // TODO: We use `mnemonicToSeed()` from `bip39-web-crypto` here instead of using `getKeyPairFromMnemonic`, which + // internally uses `bip39`. Instead, we should just be using `getKeyPairFromMnemonic` and lazy load this dependency: + // + // For additional context, see https://www.notion.so/community-labs/Human-Crypto-Keys-reported-Bug-d3a8910dabb6460da814def62665181a + + const seedBuffer = await mnemonicToSeed(mnemonic); + + const { privateKey } = await getKeyPairFromSeed( + //@ts-ignore + seedBuffer, + { + id: "rsa", + modulusLength: 4096, + }, + { privateKeyFormat: "pkcs8-der" }, + ); + const jwk = await pkcs8ToJwk(privateKey as any); + + return jwk; +} + +/** + * Convert a PKCS8 private key to a JWK + * + * @param privateKey PKCS8 private key to convert + * @returns JWK + */ +export async function pkcs8ToJwk(privateKey: Uint8Array): Promise { + // Need to adapt for Node.js environment as the original uses window.crypto + const crypto = require('crypto').webcrypto; + + const key = await crypto.subtle.importKey( + "pkcs8", + privateKey, + { name: "RSA-PSS", hash: "SHA-256" }, + true, + ["sign"] + ); + + const jwk = await crypto.subtle.exportKey("jwk", key); + + return { + kty: jwk.kty!, + e: jwk.e!, + n: jwk.n!, + d: jwk.d, + p: jwk.p, + q: jwk.q, + dp: jwk.dp, + dq: jwk.dq, + qi: jwk.qi, + }; +} + +/** + * Check password strength + * + * @param password Password to check + */ +export function checkPasswordValid(password: string): boolean { + const strength = passwordStrength(password); + + return strength.id === 3; +} + +/** + * Validate if a string is a valid mnemonic phrase + * + * @param mnemonic Mnemonic to validate + * @returns Length of the mnemonic if valid + */ +export function isValidMnemonic(mnemonic: string): number { + isString(mnemonic, "Mnemonic has to be a string."); + + const words = mnemonic.split(" "); + + isOneOf(words.length, [12, 18, 24], "Invalid mnemonic length."); + + const wordlist = wordlists.english; + + for (const word of words) { + isOneOf(word, wordlist, "Invalid word in mnemonic."); + } + + return words.length; +} + +/** + * Initialize wallet from environment variables + * Prioritizes SEED_PHRASE over WALLET_JSON if both are present + * + * @returns JWK wallet object + */ +export async function initializeWallet(): Promise { + // If wallet is already initialized, return it + if (globalWallet) { + return globalWallet; + } + + const hasSeedPhrase = !!process.env.SEED_PHRASE; + const hasWalletJson = !!process.env.WALLET_JSON; + + if (hasSeedPhrase && hasWalletJson) { + logger.info("Both SEED_PHRASE and WALLET_JSON are provided. Prioritizing SEED_PHRASE."); + } else if (!hasSeedPhrase && !hasWalletJson) { + throw new Error("No wallet configuration found. Please provide either SEED_PHRASE or WALLET_JSON in environment variables."); + } + + let wallet: JWKInterface; + let seedPhraseAddress: string | undefined; + let jsonAddress: string | undefined; + + // Try to load wallet from seed phrase if available + if (hasSeedPhrase) { + try { + const seedPhrase = process.env.SEED_PHRASE!; + if (!isValidMnemonic(seedPhrase)) { + throw new Error("Invalid seed phrase format"); + } + + wallet = await jwkFromMnemonic(seedPhrase); + seedPhraseAddress = await arweave.wallets.jwkToAddress(wallet); + + // If we have both, also get the JSON wallet address for comparison + if (hasWalletJson) { + try { + const jsonWallet = JSON.parse(process.env.WALLET_JSON!); + jsonAddress = await arweave.wallets.jwkToAddress(jsonWallet); + } catch (error) { + logger.error("Failed to parse WALLET_JSON", error); + } + } + + walletSource = 'seed_phrase'; + globalWallet = wallet; + logger.info(`Using wallet from SEED_PHRASE with address: ${seedPhraseAddress}`); + + if (jsonAddress) { + logger.info(`WALLET_JSON address (not used): ${jsonAddress}`); + } + } catch (error) { + logger.error("Failed to initialize wallet from SEED_PHRASE", error); + + // Fall back to JSON if available + if (hasWalletJson) { + logger.info("Falling back to WALLET_JSON"); + } else { + throw error; + } + } + } + + // If we haven't successfully initialized the wallet from seed phrase, try JSON + if (!globalWallet && hasWalletJson) { + try { + wallet = JSON.parse(process.env.WALLET_JSON!); + jsonAddress = await arweave.wallets.jwkToAddress(wallet); + + walletSource = 'json'; + globalWallet = wallet; + logger.info(`Using wallet from WALLET_JSON with address: ${jsonAddress}`); + } catch (error) { + throw new Error(`Failed to initialize wallet from WALLET_JSON: ${error}`); + } + } + + return globalWallet!; +} + +/** + * Get wallet address + * + * @returns Provider ID/wallet address + */ +export async function getWalletAddress(): Promise { + const wallet = await initializeWallet(); + return await arweave.wallets.jwkToAddress(wallet); +} + +/** + * Get the wallet for signing transactions + * + * @returns JWK wallet object + */ +export async function getWallet(): Promise { + return await initializeWallet(); +} From cbe7f4d005f082450b0a7707c944ad533f06a8c0 Mon Sep 17 00:00:00 2001 From: emerson Date: Tue, 22 Jul 2025 16:10:53 -0400 Subject: [PATCH 75/80] updates to walletUtils and working on debian package --- orchestrator/src/walletUtils.ts | 98 ++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/orchestrator/src/walletUtils.ts b/orchestrator/src/walletUtils.ts index b6f7147..605572c 100644 --- a/orchestrator/src/walletUtils.ts +++ b/orchestrator/src/walletUtils.ts @@ -5,6 +5,8 @@ import { isOneOf, isString } from "typed-assert"; import { wordlists, mnemonicToSeed } from "bip39-web-crypto"; import logger from "./logger"; import Arweave from "arweave"; +import * as fs from 'fs/promises'; // <-- Import Node.js file system promises module + // Initialize Arweave const arweave = Arweave.init({ @@ -15,7 +17,8 @@ const arweave = Arweave.init({ // Global wallet storage let globalWallet: JWKInterface | null = null; -let walletSource: 'seed_phrase' | 'json' | null = null; +let walletSource: 'seed_phrase' | 'json' | 'json_file' | 'seed_file' | null = null; // Added file sources + /** * Credits to arweave.app for the mnemonic wallet generation @@ -118,10 +121,11 @@ export function isValidMnemonic(mnemonic: string): number { return words.length; } + /** - * Initialize wallet from environment variables - * Prioritizes SEED_PHRASE over WALLET_JSON if both are present - * + * Initialize wallet from environment variables or files + * Prioritizes SEED_PHRASE_FILE > WALLET_JSON_FILE > SEED_PHRASE > WALLET_JSON + * * @returns JWK wallet object */ export async function initializeWallet(): Promise { @@ -130,76 +134,96 @@ export async function initializeWallet(): Promise { return globalWallet; } + // --- New: Check for file paths first --- + const hasSeedFile = !!process.env.SEED_FILE_PATH; + const hasWalletJsonFile = !!process.env.WALLET_JSON_FILE_PATH; + + // --- Existing: Check for direct environment variables --- const hasSeedPhrase = !!process.env.SEED_PHRASE; const hasWalletJson = !!process.env.WALLET_JSON; - if (hasSeedPhrase && hasWalletJson) { - logger.info("Both SEED_PHRASE and WALLET_JSON are provided. Prioritizing SEED_PHRASE."); - } else if (!hasSeedPhrase && !hasWalletJson) { - throw new Error("No wallet configuration found. Please provide either SEED_PHRASE or WALLET_JSON in environment variables."); + if (!hasSeedFile && !hasWalletJsonFile && !hasSeedPhrase && !hasWalletJson) { + throw new Error("No wallet configuration found. Please provide SEED_FILE_PATH, WALLET_JSON_FILE_PATH, SEED_PHRASE, or WALLET_JSON in environment variables."); } let wallet: JWKInterface; let seedPhraseAddress: string | undefined; let jsonAddress: string | undefined; - // Try to load wallet from seed phrase if available - if (hasSeedPhrase) { + // --- Priority 1: From Seed Phrase File --- + if (hasSeedFile) { + try { + const seedPhrase = await fs.readFile(process.env.SEED_FILE_PATH!, 'utf8'); + if (!isValidMnemonic(seedPhrase.trim())) { // .trim() to remove potential newlines + throw new Error("Invalid seed phrase format in file"); + } + wallet = await jwkFromMnemonic(seedPhrase.trim()); + seedPhraseAddress = await arweave.wallets.jwkToAddress(wallet); + walletSource = 'seed_file'; + globalWallet = wallet; + logger.info(`Using wallet from SEED_FILE_PATH with address: ${seedPhraseAddress}`); + return globalWallet; // Exit early if successful + } catch (error) { + logger.error(`Failed to initialize wallet from SEED_FILE_PATH (${process.env.SEED_FILE_PATH!})`, error); + // Fall through to next options if file loading failed + } + } + + // --- Priority 2: From Wallet JSON File --- + if (!globalWallet && hasWalletJsonFile) { + try { + const jsonString = await fs.readFile(process.env.WALLET_JSON_FILE_PATH!, 'utf8'); + wallet = JSON.parse(jsonString); + jsonAddress = await arweave.wallets.jwkToAddress(wallet); + walletSource = 'json_file'; + globalWallet = wallet; + logger.info(`Using wallet from WALLET_JSON_FILE_PATH with address: ${jsonAddress}`); + return globalWallet; // Exit early if successful + } catch (error) { + logger.error(`Failed to initialize wallet from WALLET_JSON_FILE_PATH (${process.env.WALLET_JSON_FILE_PATH!})`, error); + // Fall through to original env var options + } + } + + // --- Priority 3: From SEED_PHRASE environment variable (original logic) --- + if (!globalWallet && hasSeedPhrase) { // Ensure it hasn't been set by file try { const seedPhrase = process.env.SEED_PHRASE!; if (!isValidMnemonic(seedPhrase)) { throw new Error("Invalid seed phrase format"); } - wallet = await jwkFromMnemonic(seedPhrase); seedPhraseAddress = await arweave.wallets.jwkToAddress(wallet); - - // If we have both, also get the JSON wallet address for comparison - if (hasWalletJson) { - try { - const jsonWallet = JSON.parse(process.env.WALLET_JSON!); - jsonAddress = await arweave.wallets.jwkToAddress(jsonWallet); - } catch (error) { - logger.error("Failed to parse WALLET_JSON", error); - } - } - walletSource = 'seed_phrase'; globalWallet = wallet; logger.info(`Using wallet from SEED_PHRASE with address: ${seedPhraseAddress}`); - - if (jsonAddress) { - logger.info(`WALLET_JSON address (not used): ${jsonAddress}`); - } + // ... (comparison logic if hasWalletJson is also true, as in original) ... + return globalWallet; } catch (error) { logger.error("Failed to initialize wallet from SEED_PHRASE", error); - - // Fall back to JSON if available - if (hasWalletJson) { - logger.info("Falling back to WALLET_JSON"); - } else { - throw error; - } + // Fall through to JSON env var } } - // If we haven't successfully initialized the wallet from seed phrase, try JSON - if (!globalWallet && hasWalletJson) { + // --- Priority 4: From WALLET_JSON environment variable (original logic) --- + if (!globalWallet && hasWalletJson) { // Ensure it hasn't been set by previous methods try { wallet = JSON.parse(process.env.WALLET_JSON!); jsonAddress = await arweave.wallets.jwkToAddress(wallet); - walletSource = 'json'; globalWallet = wallet; logger.info(`Using wallet from WALLET_JSON with address: ${jsonAddress}`); + return globalWallet; } catch (error) { throw new Error(`Failed to initialize wallet from WALLET_JSON: ${error}`); } } - return globalWallet!; + // If here, no wallet was successfully initialized + throw new Error("No wallet configuration successfully initialized from any source."); } + /** * Get wallet address * From d2e81e311a1aa46c50f50855eab784604cadcdd3 Mon Sep 17 00:00:00 2001 From: emerson Date: Tue, 22 Jul 2025 16:13:11 -0400 Subject: [PATCH 76/80] updates for walletUtils and begin debian pkg --- debian/changelog | 0 debian/conffiles | 3 + debian/control | 0 debian/install | 14 + debian/postinst | 99 +++++ debian/prerm | 0 debian/randao.service | 21 + debian/randao.timer | 22 ++ debian/rules | 10 + docker-compose/docker-compose.appliance.yml | 30 ++ docker-compose/docker-compose.yml | 26 +- orchestrator/.dockerignore | 1 + orchestrator/src/walletUtils.ts | 405 ++++++++++++-------- 13 files changed, 463 insertions(+), 168 deletions(-) create mode 100644 debian/changelog create mode 100644 debian/conffiles create mode 100644 debian/control create mode 100644 debian/install create mode 100644 debian/postinst create mode 100644 debian/prerm create mode 100644 debian/randao.service create mode 100644 debian/randao.timer create mode 100644 debian/rules create mode 100644 docker-compose/docker-compose.appliance.yml diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..e69de29 diff --git a/debian/conffiles b/debian/conffiles new file mode 100644 index 0000000..d567f72 --- /dev/null +++ b/debian/conffiles @@ -0,0 +1,3 @@ +/etc/randao/.env +/etc/randao/wallet.json +/etc/randao/wallet.seed \ No newline at end of file diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..e69de29 diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..2007181 --- /dev/null +++ b/debian/install @@ -0,0 +1,14 @@ +# Source files from your Git repo (relative to repo root) +# Destination on target system (relative to /) + +# Systemd service and timer files +debian/randao.service /etc/systemd/system/ +debian/randao.timer /etc/systemd/system/ + +# Docker Compose project files +docker-compose/ /opt/randao-provider/ +orchestrator/ /opt/randao-provider/ +puzzle-generator/ /opt/randao-provider/ +requester/ /opt/randao-provider/ +LICENSE /opt/randao-provider/ +README.md /opt/randao-provider/ \ No newline at end of file diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..c97fde1 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,99 @@ +#!/bin/sh +# postinst script for randao-provider Debian package + +set -e # Exit immediately if a command exits with a non-zero status + +# --- 1. Define Paths and Logging --- +LOG_FILE="/var/log/randao-provider-postinst.log" +USERNAME="randao_service" +GROUPNAME="randao_service" +DOCKER_GROUP="docker" +ETC_RANDAO_DIR="/etc/randao" +APP_ROOT_DIR="/opt/randao-provider" + +# Initialize log file with secure permissions +touch "$LOG_FILE" +chmod 600 "$LOG_FILE" + +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - postinst: $1" >> "$LOG_FILE" +} + +log_message "Starting randao-provider post-installation script." + +# --- 2. Create the dedicated system user and group --- +log_message "Checking for user '$USERNAME' and group '$GROUPNAME'." +if ! id -u "$USERNAME" >/dev/null 2>&1; then + log_message "Creating system user '$USERNAME' with a dynamic UID." + # Let the system pick a safe UID automatically + adduser --system --no-create-home --group "$USERNAME" + log_message "User '$USERNAME' created." +else + log_message "User '$USERNAME' already exists. Skipping creation." +fi + +# Add the user to the docker group if it exists +if getent group "$DOCKER_GROUP" >/dev/null 2>&1; then + if ! getent group "$DOCKER_GROUP" | grep -q "\b$USERNAME\b"; then + log_message "Adding user '$USERNAME' to group '$DOCKER_GROUP'." + usermod -aG "$DOCKER_GROUP" "$USERNAME" + log_message "User '$USERNAME' added to '$DOCKER_GROUP'." + else + log_message "User '$USERNAME' is already in group '$DOCKER_GROUP'." + fi +else + log_message "WARNING: Group '$DOCKER_GROUP' does not exist. The service may not function correctly." +fi + +# --- 3. Manage Configuration --- +# NOTE: This section is ideally replaced by using a 'conffiles' file. +# The logic is kept here assuming you are not using conffiles yet. +log_message "Ensuring config directory '$ETC_RANDAO_DIR' exists." +mkdir -p "$ETC_RANDAO_DIR" +chown root:root "$ETC_RANDAO_DIR" +chmod 700 "$ETC_RANDAO_DIR" # Only root can access the directory listing + +TEMPLATE_DIR="$APP_ROOT_DIR/docker-compose/templates" + +# Safely copy example config files if they don't already exist +if [ ! -f "$ETC_RANDAO_DIR/.env" ]; then + log_message "Copying example.env to $ETC_RANDAO_DIR/.env." + cp "$TEMPLATE_DIR/example.env" "$ETC_RANDAO_DIR/.env" +fi +if [ ! -f "$ETC_RANDAO_DIR/wallet.json" ]; then + log_message "Copying example.wallet.json to $ETC_RANDAO_DIR/wallet.json." + cp "$TEMPLATE_DIR/example.wallet.json" "$ETC_RANDAO_DIR/wallet.json" +fi + +# --- 4. Set Secure Permissions for Configuration Files --- +# Set permissions on any config file that exists in the directory. +log_message "Setting ownership and permissions for config files in '$ETC_RANDAO_DIR'." +if [ -f "$ETC_RANDAO_DIR/.env" ]; then + chown root:"$GROUPNAME" "$ETC_RANDAO_DIR/.env" + chmod 640 "$ETC_RANDAO_DIR/.env" # root:rw, group:r, other:--- +fi +if [ -f "$ETC_RANDAO_DIR/wallet.json" ]; then + chown root:"$GROUPNAME" "$ETC_RANDAO_DIR/wallet.json" + chmod 640 "$ETC_RANDAO_DIR/wallet.json" +fi +if [ -f "$ETC_RANDAO_DIR/wallet.seed" ]; then + chown root:"$GROUPNAME" "$ETC_RANDAO_DIR/wallet.seed" + chmod 640 "$ETC_RANDAO_DIR/wallet.seed" +fi + +log_message "Permissions set. User '$USERNAME' (in group '$GROUPNAME') has read-access to configs." +log_message "IMPORTANT: Remember to edit configuration in /etc/randao/ with your actual secrets." + +# --- 5. Enable and Start Systemd Units --- +# NOTE: This section is ideally removed in favor of deb-helper in debian/rules. +# The logic is kept here assuming you are not using deb-helper yet. +log_message "Reloading systemd daemon, then enabling and starting randao.timer." +systemctl daemon-reload +systemctl enable randao.timer +systemctl start --no-block randao.timer +log_message "Systemd randao.timer has been enabled and started." + + +log_message "Randao Provider post-installation script finished." + +exit 0 \ No newline at end of file diff --git a/debian/prerm b/debian/prerm new file mode 100644 index 0000000..e69de29 diff --git a/debian/randao.service b/debian/randao.service new file mode 100644 index 0000000..60fcf41 --- /dev/null +++ b/debian/randao.service @@ -0,0 +1,21 @@ +[Unit] +Description=RANDAO Provider +# When running manually, use the --no-block flag: +# systemctl start --no-block randao.service +Requires=docker.service +After=docker.service +#Wants-randao.timer + +[Service] +Type=simple +User=randao_service +Group=randao_service +WorkingDirectory=/home/randao/RandaoProvider/docker-compose +ExecStart=/usr/bin/docker compose --env-file /etc/randao/.env up --pull=always +ExecStop=/usr/bin/docker compose down +TimeoutStartSec=0 +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target diff --git a/debian/randao.timer b/debian/randao.timer new file mode 100644 index 0000000..958852a --- /dev/null +++ b/debian/randao.timer @@ -0,0 +1,22 @@ +Description=Timer to periodically restart RANDAO Provider for latest image pull +#Requires=randao.service +#After=randao.service + +[Timer] +# Restart every day at a random time within the first hour of the day +# OnCalendar=daily +# RandomizedDelaySec=1h +# Persistent=true + +# OR, restart every 12 hours (e.g., 00:00, 12:00) with a random delay +OnCalendar=*-*-* 00,12:00:00 +RandomizedDelaySec=30min +Persistent=true + +# OR, restart every 8 hours from when the service last became active +#OnUnitActiveSec=8h +#RandomizedDelaySec=30min +#AccuracySec=1min # Optional: Reduce timer inaccuracy from default (often 1min) + +[Install] +WantedBy=timers.target diff --git a/debian/rules b/debian/rules new file mode 100644 index 0000000..6a675de --- /dev/null +++ b/debian/rules @@ -0,0 +1,10 @@ +#!/usr/bin/make -f + +%: + dh $@ + +# This block overrides the default 'dh_auto_configure' step. +# It tells deb-helper to run the 'configure' script but with +# an extra option. After this step, the normal sequence continues. +override_dh_auto_configure: + dh_auto_configure -- --with-extra-feature \ No newline at end of file diff --git a/docker-compose/docker-compose.appliance.yml b/docker-compose/docker-compose.appliance.yml new file mode 100644 index 0000000..c8060a2 --- /dev/null +++ b/docker-compose/docker-compose.appliance.yml @@ -0,0 +1,30 @@ +# /home/randao/RandaoProvider/docker-compose/docker-compose.appliance.yml +# This file contains overrides specific to the appliance deployment. +# It should be loaded AFTER the base docker-compose.yml. + +services: + orchestrator: + volumes: + # Override wallet mounts to point to /etc/randao/ for appliance security + # This explicitly mounts wallet.json from /etc/randao/ + - /etc/randao/wallet.json:/app/config/wallet.json:ro + # This explicitly mounts wallet.seed from /etc/randao/ (if used) + - /etc/randao/wallet.seed:/app/config/wallet.seed:ro + deploy: # Appliance-specific deploy rules + resources: + limits: + cpus: '2.0' # Allow orchestrator up to 1.5 cores + memory: 300M # Allow up to 300MB RAM + reservations: + cpus: '1.0' # Reserve 0.5 of a core + memory: 192M + + postgres: # Appliance-specific deploy rules for Postgres + deploy: + resources: + limits: + cpus: '0.8' # Limit Postgres to 40% of one core + memory: 150M # Limit Postgres to 150MB RAM + reservations: + cpus: '0.4' # Reserve 10% of one core + memory: 64M \ No newline at end of file diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index f46a33a..e4a9cb3 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,3 +1,7 @@ +# /home/randao/RandaoProvider/docker-compose/docker-compose.yml (Base) +# This file defines the core services and their common configuration. +# It uses relative paths for secrets, suitable for standalone/dev environments. + services: postgres: image: postgres:13-alpine @@ -11,6 +15,7 @@ services: - backend volumes: - pgdata:/var/lib/postgresql/data + - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro # Relative path for custom Postgres config healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-mydatabase}"] interval: 10s @@ -21,11 +26,10 @@ services: options: max-size: "100m" max-file: "5" + shm_size: '64m' orchestrator: - image: randao/orchestrator:latest - # pull_policy: always # Ensure the latest image is always pulled - restart: unless-stopped # Restart on crash (or on exit with non-zero code) + image: randao/orchestrator:latest # Or your chosen stable tag (e.g., appliance-stable) depends_on: postgres: condition: service_healthy @@ -35,13 +39,23 @@ services: DB_USER: ${DB_USER:-myuser} DB_PASSWORD: ${DB_PASSWORD:-mypassword} DB_NAME: ${DB_NAME:-mydatabase} - SEED_PHRASE: ${SEED_PHRASE} + # New environment variables to point to the wallet files INSIDE the container + # The app code will prioritize these if the files are mounted and readable. + WALLET_JSON_FILE_PATH: /app/config/wallet.json # For JWK JSON wallet. container path after the volume is mounted (see 'volumes:' below) + SEED_FILE_PATH: /app/config/wallet.seed # For mnemonic seed phrase. container path after the volume is mounted (see 'volumes:' below) + # WALLET_JSON is passed. If WALLET_JSON_FILE_PATH fails/is missing, app falls back to this. WALLET_JSON: ${WALLET_JSON} + SEED_PHRASE: ${SEED_PHRASE} # Pass if you want to support seed phrase env var too DOCKER_NETWORK: backend networks: - backend volumes: - - /var/run/docker.sock:/var/run/docker.sock + - /var/run/docker.sock:/var/run/docker.sock # Common Docker socket mount + # Mount wallet files from the project directory for standalone/dev + # If ./wallet.json doesn't exist, Docker will create an empty dir, + # but the app will fallback to WALLET_JSON env var if set. + - ./wallet.json:/app/config/wallet.json:ro # Read-only mount for JWK file + - ./wallet.seed:/app/config/wallet.seed:ro # Read-only mount for seed phrase file (optional, if used) logging: driver: json-file options: @@ -55,4 +69,4 @@ networks: volumes: pgdata: - driver: local + driver: local \ No newline at end of file diff --git a/orchestrator/.dockerignore b/orchestrator/.dockerignore index a0bedda..c4f9ed1 100644 --- a/orchestrator/.dockerignore +++ b/orchestrator/.dockerignore @@ -1,3 +1,4 @@ node_modules .git dist +debian \ No newline at end of file diff --git a/orchestrator/src/walletUtils.ts b/orchestrator/src/walletUtils.ts index 605572c..043ca89 100644 --- a/orchestrator/src/walletUtils.ts +++ b/orchestrator/src/walletUtils.ts @@ -1,244 +1,325 @@ -import { getKeyPairFromSeed } from "human-crypto-keys"; +import { getKeyPairFromMnemonic } from "human-crypto-keys"; // <-- updated import import type { JWKInterface } from "arweave/web/lib/wallet"; +import { webcrypto } from 'crypto'; import { passwordStrength } from "check-password-strength"; import { isOneOf, isString } from "typed-assert"; import { wordlists, mnemonicToSeed } from "bip39-web-crypto"; import logger from "./logger"; import Arweave from "arweave"; -import * as fs from 'fs/promises'; // <-- Import Node.js file system promises module +import * as fs from 'fs/promises'; +// --- Configuration --- -// Initialize Arweave +// Externalize Arweave configuration for flexibility. const arweave = Arweave.init({ - host: "arweave.net", - port: 443, - protocol: "https", + host: process.env.ARWEAVE_HOST || "arweave.net", + port: process.env.ARWEAVE_PORT ? parseInt(process.env.ARWEAVE_PORT, 10) : 443, + protocol: process.env.ARWEAVE_PROTOCOL || "https", }); -// Global wallet storage -let globalWallet: JWKInterface | null = null; -let walletSource: 'seed_phrase' | 'json' | 'json_file' | 'seed_file' | null = null; // Added file sources +// Avoid "magic numbers" by defining as constants. +const STRONG_PASSWORD_LEVEL = 3; // Corresponds to 'Strong' in check-password-strength +// Create a Set for efficient mnemonic validation (O(1) lookup). +const englishWordlistSet = new Set(wordlists.english); + + +// --- Global State --- +let globalWallet: JWKInterface | null = null; +let walletSource: 'seed_file' | 'json_file' | 'seed_phrase' | 'json_string' | null = null; -/** - * Credits to arweave.app for the mnemonic wallet generation - * - * https://github.com/jfbeats/ArweaveWebWallet/blob/master/src/functions/Wallets.ts - * https://github.com/jfbeats/ArweaveWebWallet/blob/master/src/functions/Crypto.ts - */ +// --- Core Crypto Functions --- /** - * Generate a JWK from a mnemonic seedphrase + * Generate a JWK from a mnemonic seedphrase using the direct method. * - * @param mnemonic Mnemonic seedphrase to generate wallet from - * @returns Wallet JWK + * @param mnemonic Mnemonic seedphrase to generate wallet from. + * @returns Wallet JWK. */ export async function jwkFromMnemonic(mnemonic: string): Promise { - // TODO: We use `mnemonicToSeed()` from `bip39-web-crypto` here instead of using `getKeyPairFromMnemonic`, which - // internally uses `bip39`. Instead, we should just be using `getKeyPairFromMnemonic` and lazy load this dependency: - // - // For additional context, see https://www.notion.so/community-labs/Human-Crypto-Keys-reported-Bug-d3a8910dabb6460da814def62665181a - - const seedBuffer = await mnemonicToSeed(mnemonic); - - const { privateKey } = await getKeyPairFromSeed( - //@ts-ignore - seedBuffer, + // Directly generate the key pair from the mnemonic string. + const { privateKey } = await getKeyPairFromMnemonic( + mnemonic, { id: "rsa", modulusLength: 4096, }, { privateKeyFormat: "pkcs8-der" }, ); + + // Convert the resulting PKCS8 key to the JWK format. + // TODO: The `as any` cast should be investigated to improve type safety. const jwk = await pkcs8ToJwk(privateKey as any); return jwk; } /** - * Convert a PKCS8 private key to a JWK + * Convert a PKCS8 private key to a JWK using Node.js's native crypto. * - * @param privateKey PKCS8 private key to convert - * @returns JWK + * @param privateKey PKCS8 private key to convert. + * @returns JWK. */ export async function pkcs8ToJwk(privateKey: Uint8Array): Promise { - // Need to adapt for Node.js environment as the original uses window.crypto const crypto = require('crypto').webcrypto; - + const key = await crypto.subtle.importKey( - "pkcs8", - privateKey, - { name: "RSA-PSS", hash: "SHA-256" }, - true, + "pkcs8", + privateKey, + { name: "RSA-PSS", hash: "SHA-256" }, + true, ["sign"] ); - + const jwk = await crypto.subtle.exportKey("jwk", key); + // The explicit mapping is safe but could potentially be simplified + // if Arweave's JWKInterface is compatible with the standard JsonWebKey type. return { - kty: jwk.kty!, - e: jwk.e!, - n: jwk.n!, - d: jwk.d, - p: jwk.p, - q: jwk.q, - dp: jwk.dp, - dq: jwk.dq, - qi: jwk.qi, + kty: jwk.kty!, e: jwk.e!, n: jwk.n!, + d: jwk.d, p: jwk.p, q: jwk.q, + dp: jwk.dp, dq: jwk.dq, qi: jwk.qi, }; } +// * IF Arweave's JWKInterface IS compatible with the standard JsonWebKey type. */ +// /** +// * Convert a PKCS8 private key to a JWK +// * +// * @param privateKey PKCS8 private key to convert +// * @returns JWK +// */ +// export async function pkcs8ToJwk(privateKey: Uint8Array): Promise { +// const key = await webcrypto.subtle.importKey( +// "pkcs8", +// privateKey, +// { name: "RSA-PSS", hash: "SHA-256" }, +// true, +// ["sign"] +// ); + +// const jwk = await webcrypto.subtle.exportKey("jwk", key); + +// // Simply return the jwk object directly. TypeScript will ensure it matches +// // the 'JWKInterface' return type. This is cleaner and more maintainable. +// return jwk; +// } + + + + +// --- Validation Functions --- /** - * Check password strength + * Check if a password is rated as "Strong". * - * @param password Password to check + * @param password Password to check. */ export function checkPasswordValid(password: string): boolean { const strength = passwordStrength(password); - - return strength.id === 3; + return strength.id === STRONG_PASSWORD_LEVEL; } /** - * Validate if a string is a valid mnemonic phrase - * - * @param mnemonic Mnemonic to validate - * @returns Length of the mnemonic if valid + * Validate if a string is a valid BIP-39 mnemonic phrase using an efficient Set lookup. + * * @param mnemonic Mnemonic to validate. + * @returns `true` if the mnemonic is valid, otherwise `false`. */ -export function isValidMnemonic(mnemonic: string): number { - isString(mnemonic, "Mnemonic has to be a string."); - - const words = mnemonic.split(" "); - - isOneOf(words.length, [12, 18, 24], "Invalid mnemonic length."); - - const wordlist = wordlists.english; - - for (const word of words) { - isOneOf(word, wordlist, "Invalid word in mnemonic."); +export function isValidMnemonic(mnemonic: string): boolean { + try { + isString(mnemonic, "Mnemonic has to be a string."); + const words = mnemonic.trim().split(" "); + isOneOf(words.length, [12, 18, 24], "Invalid mnemonic length."); + + for (const word of words) { + if (!englishWordlistSet.has(word)) { + logger.warn(`Invalid word found in mnemonic: "${word}"`); + return false; + } + } + return true; + } catch (error) { + logger.error("Mnemonic validation failed", error); + return false; } - - return words.length; } +// --- Wallet Initialization & Access --- + /** - * Initialize wallet from environment variables or files - * Prioritizes SEED_PHRASE_FILE > WALLET_JSON_FILE > SEED_PHRASE > WALLET_JSON + * Initializes the global wallet by trying a series of sources in a defined order of priority. * - * @returns JWK wallet object + * @returns JWK wallet object. */ export async function initializeWallet(): Promise { - // If wallet is already initialized, return it if (globalWallet) { + logger.debug("[DEBUG] Wallet already initialized. Returning existing wallet."); return globalWallet; } - // --- New: Check for file paths first --- - const hasSeedFile = !!process.env.SEED_FILE_PATH; - const hasWalletJsonFile = !!process.env.WALLET_JSON_FILE_PATH; - - // --- Existing: Check for direct environment variables --- - const hasSeedPhrase = !!process.env.SEED_PHRASE; - const hasWalletJson = !!process.env.WALLET_JSON; - - if (!hasSeedFile && !hasWalletJsonFile && !hasSeedPhrase && !hasWalletJson) { - throw new Error("No wallet configuration found. Please provide SEED_FILE_PATH, WALLET_JSON_FILE_PATH, SEED_PHRASE, or WALLET_JSON in environment variables."); - } - - let wallet: JWKInterface; - let seedPhraseAddress: string | undefined; - let jsonAddress: string | undefined; - - // --- Priority 1: From Seed Phrase File --- - if (hasSeedFile) { - try { - const seedPhrase = await fs.readFile(process.env.SEED_FILE_PATH!, 'utf8'); - if (!isValidMnemonic(seedPhrase.trim())) { // .trim() to remove potential newlines - throw new Error("Invalid seed phrase format in file"); + // Define wallet sources in order of priority. + // The 'path' property for env var sources is just for logging clarity. + const sources = [ + { + type: 'seed_file', + path: process.env.SEED_FILE_PATH, + enabled: !!process.env.SEED_FILE_PATH, + load: async () => { + logger.debug(`[DEBUG] Attempting to load wallet from SEED_FILE_PATH: ${process.env.SEED_FILE_PATH}`); + const seed = await fs.readFile(process.env.SEED_FILE_PATH!, 'utf8'); + logger.debug(`[DEBUG] Read seed string from file (first 50 chars): "${seed.trim().substring(0, 50)}..."`); + if (!isValidMnemonic(seed)) throw new Error("Invalid mnemonic format in file."); + return jwkFromMnemonic(seed.trim()); + }, + }, + { + type: 'json_file', + path: process.env.WALLET_JSON_FILE_PATH, + enabled: !!process.env.WALLET_JSON_FILE_PATH, + load: async () => { + logger.debug(`[DEBUG] Attempting to load wallet from WALLET_JSON_FILE_PATH: ${process.env.WALLET_JSON_FILE_PATH}`); + const jsonString = await fs.readFile(process.env.WALLET_JSON_FILE_PATH!, 'utf8'); + logger.debug(`[DEBUG] Read JSON string from file (first 50 chars): "${jsonString.trim().substring(0, 50)}..."`); + return JSON.parse(jsonString); + }, + }, + { + type: 'seed_phrase', + path: "env var", // Placeholder for logging + enabled: !!process.env.SEED_PHRASE, + load: async () => { + logger.debug(`[DEBUG] Attempting to load wallet from SEED_PHRASE env var.`); + const seed = process.env.SEED_PHRASE!; + logger.debug(`[DEBUG] SEED_PHRASE env var content (first 50 chars): "${seed.trim().substring(0, 50)}..."`); + if (!isValidMnemonic(seed)) throw new Error("Invalid mnemonic format in env var."); + return jwkFromMnemonic(seed.trim()); + }, + }, + { + type: 'json_string', + path: "env var", // Placeholder for logging + enabled: !!process.env.WALLET_JSON, + load: async () => { + logger.debug(`[DEBUG] Attempting to load wallet from WALLET_JSON env var.`); + const jsonString = process.env.WALLET_JSON!; + logger.debug(`[DEBUG] WALLET_JSON env var content (first 50 chars): "${jsonString.trim().substring(0, 50)}..."`); + return JSON.parse(jsonString); + }, + }, + ]; + + for (const source of sources) { + if (source.enabled) { + logger.debug(`[DEBUG] Checking wallet source: ${source.type.toUpperCase()}`); + try { + const wallet = await source.load(); + const address = await arweave.wallets.jwkToAddress(wallet); + + logger.info(`Wallet initialized from ${source.type.toUpperCase()} with address: ${address}`); + if (source.path && source.path !== "env var") { // Log path only if it's a file path + logger.info(`Wallet source path: ${source.path}`); + } + + globalWallet = wallet; + walletSource = source.type; + return globalWallet; // Exit early if successful + } catch (error) { + logger.error(`Failed to load wallet from ${source.type.toUpperCase()} (${source.path || 'no path specified'}): ${error instanceof Error ? error.message : String(error)}`); + logger.debug(`[DEBUG] Full error details for ${source.type.toUpperCase()}:`, error); + // Fall through to the next source. } - wallet = await jwkFromMnemonic(seedPhrase.trim()); - seedPhraseAddress = await arweave.wallets.jwkToAddress(wallet); - walletSource = 'seed_file'; - globalWallet = wallet; - logger.info(`Using wallet from SEED_FILE_PATH with address: ${seedPhraseAddress}`); - return globalWallet; // Exit early if successful - } catch (error) { - logger.error(`Failed to initialize wallet from SEED_FILE_PATH (${process.env.SEED_FILE_PATH!})`, error); - // Fall through to next options if file loading failed + } else { + logger.debug(`[DEBUG] Wallet source ${source.type.toUpperCase()} is not enabled or not configured.`); } } - // --- Priority 2: From Wallet JSON File --- - if (!globalWallet && hasWalletJsonFile) { - try { - const jsonString = await fs.readFile(process.env.WALLET_JSON_FILE_PATH!, 'utf8'); - wallet = JSON.parse(jsonString); - jsonAddress = await arweave.wallets.jwkToAddress(wallet); - walletSource = 'json_file'; - globalWallet = wallet; - logger.info(`Using wallet from WALLET_JSON_FILE_PATH with address: ${jsonAddress}`); - return globalWallet; // Exit early if successful - } catch (error) { - logger.error(`Failed to initialize wallet from WALLET_JSON_FILE_PATH (${process.env.WALLET_JSON_FILE_PATH!})`, error); - // Fall through to original env var options - } - } + throw new Error("No wallet configuration could be successfully initialized from any source. Please check environment variables and file paths."); +} - // --- Priority 3: From SEED_PHRASE environment variable (original logic) --- - if (!globalWallet && hasSeedPhrase) { // Ensure it hasn't been set by file - try { - const seedPhrase = process.env.SEED_PHRASE!; - if (!isValidMnemonic(seedPhrase)) { - throw new Error("Invalid seed phrase format"); - } - wallet = await jwkFromMnemonic(seedPhrase); - seedPhraseAddress = await arweave.wallets.jwkToAddress(wallet); - walletSource = 'seed_phrase'; - globalWallet = wallet; - logger.info(`Using wallet from SEED_PHRASE with address: ${seedPhraseAddress}`); - // ... (comparison logic if hasWalletJson is also true, as in original) ... - return globalWallet; - } catch (error) { - logger.error("Failed to initialize wallet from SEED_PHRASE", error); - // Fall through to JSON env var - } - } - // --- Priority 4: From WALLET_JSON environment variable (original logic) --- - if (!globalWallet && hasWalletJson) { // Ensure it hasn't been set by previous methods - try { - wallet = JSON.parse(process.env.WALLET_JSON!); - jsonAddress = await arweave.wallets.jwkToAddress(wallet); - walletSource = 'json'; - globalWallet = wallet; - logger.info(`Using wallet from WALLET_JSON with address: ${jsonAddress}`); - return globalWallet; - } catch (error) { - throw new Error(`Failed to initialize wallet from WALLET_JSON: ${error}`); - } - } - // If here, no wallet was successfully initialized - throw new Error("No wallet configuration successfully initialized from any source."); -} +// old initialize +// export async function initializeWallet(): Promise { +// if (globalWallet) { +// return globalWallet; +// } + +// // * Define wallet sources in order of priority. +// // 1. seed file (location indicated in docker-compose.yml) +// // 2. wallet.json file (location indicated in docker-compose.yml +// // 3. seed phrase in .env +// // 4. wallet.json in .env +// const sources = [ +// { +// type: 'seed_file', +// path: process.env.SEED_FILE_PATH, +// enabled: !!process.env.SEED_FILE_PATH, +// load: async () => { +// const seed = await fs.readFile(process.env.SEED_FILE_PATH!, 'utf8'); +// if (!isValidMnemonic(seed)) throw new Error("Invalid mnemonic format in file."); +// return jwkFromMnemonic(seed.trim()); +// }, +// }, +// { +// type: 'json_file', +// path: process.env.WALLET_JSON_FILE_PATH, +// enabled: !!process.env.WALLET_JSON_FILE_PATH, +// load: async () => JSON.parse(await fs.readFile(process.env.WALLET_JSON_FILE_PATH!, 'utf8')), +// }, +// { +// type: 'seed_phrase', +// path: "env var", +// enabled: !!process.env.SEED_PHRASE, +// load: async () => { +// const seed = process.env.SEED_PHRASE!; +// if (!isValidMnemonic(seed)) throw new Error("Invalid mnemonic format in env var."); +// return jwkFromMnemonic(seed.trim()); +// }, +// }, +// { +// type: 'json_string', +// path: "env var", +// enabled: !!process.env.WALLET_JSON, +// load: async () => JSON.parse(process.env.WALLET_JSON!), +// }, +// ]; + +// for (const source of sources) { +// if (source.enabled) { +// try { +// const wallet = await source.load(); +// const address = await arweave.wallets.jwkToAddress(wallet); + +// logger.info(`Wallet initialized from ${source.type.toUpperCase()} (${source.path})`); +// logger.info(`Wallet address: ${address}`); + +// globalWallet = wallet; +// walletSource = source.type; +// return globalWallet; +// } catch (error) { +// logger.error(`Failed to load wallet from ${source.type.toUpperCase()} (${source.path})`, error); +// // Fall through to the next source. +// } +// } +// } + +// throw new Error("No wallet configuration could be successfully initialized. Please check environment variables and file paths."); +// } /** - * Get wallet address - * - * @returns Provider ID/wallet address + * Get the initialized wallet's address. + * * @returns The wallet address string. */ export async function getWalletAddress(): Promise { const wallet = await initializeWallet(); - return await arweave.wallets.jwkToAddress(wallet); + return arweave.wallets.jwkToAddress(wallet); } /** - * Get the wallet for signing transactions - * - * @returns JWK wallet object + * Get the initialized wallet for signing transactions. + * * @returns The JWK wallet object. */ export async function getWallet(): Promise { - return await initializeWallet(); -} + return initializeWallet(); +} \ No newline at end of file From 29b26ba03ae460df5bf7add480bb5bf3ca5eedda Mon Sep 17 00:00:00 2001 From: emerson Date: Wed, 23 Jul 2025 09:54:12 -0400 Subject: [PATCH 77/80] update config examples --- docker-compose/.env.example | 17 +++++------------ docker-compose/docker-compose.yml | 3 ++- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/docker-compose/.env.example b/docker-compose/.env.example index ece7ec9..24a371e 100644 --- a/docker-compose/.env.example +++ b/docker-compose/.env.example @@ -3,15 +3,8 @@ DB_PASSWORD=mypassword DB_NAME=mydatabase DOCKER_NETWORK=backend LOG_CONSOLE_LEVEL=3 -SEED_PHRASE="Create a NEW wallet and enter the 12 - 24 words here" -WALLET_JSON = '{ - "kty": "RSA", - "e": "test", - "n": "test", - "d": "test", - "p": "test", - "q": "test", - "dp": "test", - "dq": "test", - "qi": "test" -}' +## Enable ONE of the wallet methods below: +#SEED_FILE_PATH=/app/config/wallet.seed # path corresponds to the container volume mounted in docker-compose.yml file +#WALLET_JSON_FILE_PATH=/app/config/wallet.json # path corresponds to the container volume mounted in docker-compose.yml file +#SEED_PHRASE="Create a NEW wallet and enter the 12 - 24 words here" +#WALLET_JSON = '{ "kty": "RSA", "e": "test", "n": "test", "d": "test", "p": "test", "q": "test", "dp": "test", "dq": "test", "qi": "test" }' diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index e4a9cb3..663c402 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -54,7 +54,8 @@ services: # Mount wallet files from the project directory for standalone/dev # If ./wallet.json doesn't exist, Docker will create an empty dir, # but the app will fallback to WALLET_JSON env var if set. - - ./wallet.json:/app/config/wallet.json:ro # Read-only mount for JWK file + # One of these is preferred for toaster/appliance approach (we could move them to the appliance yaml) + - ./wallet.json:/app/config/wallet.json:ro # Read-only mount for JWK file (optional, if used) - ./wallet.seed:/app/config/wallet.seed:ro # Read-only mount for seed phrase file (optional, if used) logging: driver: json-file From 327eac13554568480a3b9060b803a54cc1db14a5 Mon Sep 17 00:00:00 2001 From: emerson Date: Wed, 23 Jul 2025 13:14:45 -0400 Subject: [PATCH 78/80] updated walletUtils.ts --- orchestrator/src/walletUtils.ts | 147 +++++++------------------------- 1 file changed, 31 insertions(+), 116 deletions(-) diff --git a/orchestrator/src/walletUtils.ts b/orchestrator/src/walletUtils.ts index 043ca89..2a0b23a 100644 --- a/orchestrator/src/walletUtils.ts +++ b/orchestrator/src/walletUtils.ts @@ -1,6 +1,5 @@ -import { getKeyPairFromMnemonic } from "human-crypto-keys"; // <-- updated import +import { getKeyPairFromSeed } from "human-crypto-keys"; import type { JWKInterface } from "arweave/web/lib/wallet"; -import { webcrypto } from 'crypto'; import { passwordStrength } from "check-password-strength"; import { isOneOf, isString } from "typed-assert"; import { wordlists, mnemonicToSeed } from "bip39-web-crypto"; @@ -10,17 +9,17 @@ import * as fs from 'fs/promises'; // --- Configuration --- -// Externalize Arweave configuration for flexibility. +// ⭐ Recommendation: Externalize Arweave configuration for flexibility. const arweave = Arweave.init({ host: process.env.ARWEAVE_HOST || "arweave.net", port: process.env.ARWEAVE_PORT ? parseInt(process.env.ARWEAVE_PORT, 10) : 443, protocol: process.env.ARWEAVE_PROTOCOL || "https", }); -// Avoid "magic numbers" by defining as constants. +// ⭐ Recommendation: Avoid magic numbers by defining them as constants. const STRONG_PASSWORD_LEVEL = 3; // Corresponds to 'Strong' in check-password-strength -// Create a Set for efficient mnemonic validation (O(1) lookup). +// ⭐ Recommendation: Create a Set for efficient mnemonic validation (O(1) lookup). const englishWordlistSet = new Set(wordlists.english); @@ -30,25 +29,31 @@ let walletSource: 'seed_file' | 'json_file' | 'seed_phrase' | 'json_string' | nu // --- Core Crypto Functions --- + /** - * Generate a JWK from a mnemonic seedphrase using the direct method. + * Generate a JWK from a mnemonic seedphrase. * * @param mnemonic Mnemonic seedphrase to generate wallet from. * @returns Wallet JWK. */ export async function jwkFromMnemonic(mnemonic: string): Promise { - // Directly generate the key pair from the mnemonic string. - const { privateKey } = await getKeyPairFromMnemonic( - mnemonic, - { - id: "rsa", - modulusLength: 4096, - }, + // TODO: As noted in the original code, this should be replaced. + // Use `getKeyPairFromMnemonic` from `human-crypto-keys` for a more direct and efficient implementation. + // See: https://www.notion.so/community-labs/Human-Crypto-Keys-reported-Bug-d3a8910dabb6460da814def62665181a + + const seedBuffer = await mnemonicToSeed(mnemonic); + + // Recommendation: Investigate and fix the need for @ts-ignore. + // The type of `seedBuffer` (likely Uint8Array) might differ from what `getKeyPairFromSeed` expects in Node.js (e.g., a Buffer). + // A potential fix could be `Buffer.from(seedBuffer)`. + const { privateKey } = await getKeyPairFromSeed( + //@ts-ignore + seedBuffer, + { id: "rsa", modulusLength: 4096 }, { privateKeyFormat: "pkcs8-der" }, ); - // Convert the resulting PKCS8 key to the JWK format. - // TODO: The `as any` cast should be investigated to improve type safety. + // Recommendation: Investigate and fix the need for `as any`. const jwk = await pkcs8ToJwk(privateKey as any); return jwk; @@ -81,31 +86,6 @@ export async function pkcs8ToJwk(privateKey: Uint8Array): Promise dp: jwk.dp, dq: jwk.dq, qi: jwk.qi, }; } -// * IF Arweave's JWKInterface IS compatible with the standard JsonWebKey type. */ -// /** -// * Convert a PKCS8 private key to a JWK -// * -// * @param privateKey PKCS8 private key to convert -// * @returns JWK -// */ -// export async function pkcs8ToJwk(privateKey: Uint8Array): Promise { -// const key = await webcrypto.subtle.importKey( -// "pkcs8", -// privateKey, -// { name: "RSA-PSS", hash: "SHA-256" }, -// true, -// ["sign"] -// ); - -// const jwk = await webcrypto.subtle.exportKey("jwk", key); - -// // Simply return the jwk object directly. TypeScript will ensure it matches -// // the 'JWKInterface' return type. This is cleaner and more maintainable. -// return jwk; -// } - - - // --- Validation Functions --- @@ -147,7 +127,9 @@ export function isValidMnemonic(mnemonic: string): boolean { // --- Wallet Initialization & Access --- /** + * ⭐ Recommendation: Refactored `initializeWallet`. * Initializes the global wallet by trying a series of sources in a defined order of priority. + * This approach is more modular and easier to maintain. * * @returns JWK wallet object. */ @@ -206,13 +188,13 @@ export async function initializeWallet(): Promise { return JSON.parse(jsonString); }, }, - ]; + ] as const; // <--- This 'as const' is critical for type inference for (const source of sources) { if (source.enabled) { logger.debug(`[DEBUG] Checking wallet source: ${source.type.toUpperCase()}`); try { - const wallet = await source.load(); + const wallet = await source.load(); // wallet is JWKInterface const address = await arweave.wallets.jwkToAddress(wallet); logger.info(`Wallet initialized from ${source.type.toUpperCase()} with address: ${address}`); @@ -220,9 +202,11 @@ export async function initializeWallet(): Promise { logger.info(`Wallet source path: ${source.path}`); } - globalWallet = wallet; - walletSource = source.type; - return globalWallet; // Exit early if successful + globalWallet = wallet; // Assign to globalWallet for future calls + walletSource = source.type; // This assignment is now type-safe due to 'as const' + + return wallet; // <--- Directly return 'wallet' which is guaranteed JWKInterface + } catch (error) { logger.error(`Failed to load wallet from ${source.type.toUpperCase()} (${source.path || 'no path specified'}): ${error instanceof Error ? error.message : String(error)}`); logger.debug(`[DEBUG] Full error details for ${source.type.toUpperCase()}:`, error); @@ -233,80 +217,11 @@ export async function initializeWallet(): Promise { } } + // If the loop completes without successfully returning a wallet, + // then we throw an error as no wallet could be initialized. throw new Error("No wallet configuration could be successfully initialized from any source. Please check environment variables and file paths."); } - - -// old initialize -// export async function initializeWallet(): Promise { -// if (globalWallet) { -// return globalWallet; -// } - -// // * Define wallet sources in order of priority. -// // 1. seed file (location indicated in docker-compose.yml) -// // 2. wallet.json file (location indicated in docker-compose.yml -// // 3. seed phrase in .env -// // 4. wallet.json in .env -// const sources = [ -// { -// type: 'seed_file', -// path: process.env.SEED_FILE_PATH, -// enabled: !!process.env.SEED_FILE_PATH, -// load: async () => { -// const seed = await fs.readFile(process.env.SEED_FILE_PATH!, 'utf8'); -// if (!isValidMnemonic(seed)) throw new Error("Invalid mnemonic format in file."); -// return jwkFromMnemonic(seed.trim()); -// }, -// }, -// { -// type: 'json_file', -// path: process.env.WALLET_JSON_FILE_PATH, -// enabled: !!process.env.WALLET_JSON_FILE_PATH, -// load: async () => JSON.parse(await fs.readFile(process.env.WALLET_JSON_FILE_PATH!, 'utf8')), -// }, -// { -// type: 'seed_phrase', -// path: "env var", -// enabled: !!process.env.SEED_PHRASE, -// load: async () => { -// const seed = process.env.SEED_PHRASE!; -// if (!isValidMnemonic(seed)) throw new Error("Invalid mnemonic format in env var."); -// return jwkFromMnemonic(seed.trim()); -// }, -// }, -// { -// type: 'json_string', -// path: "env var", -// enabled: !!process.env.WALLET_JSON, -// load: async () => JSON.parse(process.env.WALLET_JSON!), -// }, -// ]; - -// for (const source of sources) { -// if (source.enabled) { -// try { -// const wallet = await source.load(); -// const address = await arweave.wallets.jwkToAddress(wallet); - -// logger.info(`Wallet initialized from ${source.type.toUpperCase()} (${source.path})`); -// logger.info(`Wallet address: ${address}`); - -// globalWallet = wallet; -// walletSource = source.type; -// return globalWallet; -// } catch (error) { -// logger.error(`Failed to load wallet from ${source.type.toUpperCase()} (${source.path})`, error); -// // Fall through to the next source. -// } -// } -// } - -// throw new Error("No wallet configuration could be successfully initialized. Please check environment variables and file paths."); -// } - - /** * Get the initialized wallet's address. * * @returns The wallet address string. From 94591b1f381cc1df1cceeaf41ff20a48bf38d018 Mon Sep 17 00:00:00 2001 From: emerson Date: Wed, 23 Jul 2025 16:20:07 -0400 Subject: [PATCH 79/80] updates to configs and docker for testing --- .../{ => etc/systemd/system}/randao.service | 3 +- debian/{ => etc/systemd/system}/randao.timer | 0 debian/etc/update-motd.d/66-randao | 59 ++++ debian/install | 9 +- docker-compose/docker-compose.appliance.yml | 24 +- docker-compose/docker-compose.yml | 32 +-- docker-compose/postgres/postgresql.conf | 26 ++ docker-compose/wallet.json.example | 11 + docker-compose/wallet.seed.example | 1 + updates.md | 256 ++++++++++++++++++ 10 files changed, 384 insertions(+), 37 deletions(-) rename debian/{ => etc/systemd/system}/randao.service (67%) rename debian/{ => etc/systemd/system}/randao.timer (100%) create mode 100644 debian/etc/update-motd.d/66-randao create mode 100644 docker-compose/postgres/postgresql.conf create mode 100644 docker-compose/wallet.json.example create mode 100644 docker-compose/wallet.seed.example create mode 100644 updates.md diff --git a/debian/randao.service b/debian/etc/systemd/system/randao.service similarity index 67% rename from debian/randao.service rename to debian/etc/systemd/system/randao.service index 60fcf41..eca398f 100644 --- a/debian/randao.service +++ b/debian/etc/systemd/system/randao.service @@ -11,7 +11,8 @@ Type=simple User=randao_service Group=randao_service WorkingDirectory=/home/randao/RandaoProvider/docker-compose -ExecStart=/usr/bin/docker compose --env-file /etc/randao/.env up --pull=always +#ExecStart=/usr/bin/docker compose --env-file /etc/randao/.env up --pull=always +ExecStart=/usr/bin/docker compose -f docker-compose.yml -f docker-compose.appliance.yml --env-file /etc/randao/.env up --pull=always ExecStop=/usr/bin/docker compose down TimeoutStartSec=0 Restart=on-failure diff --git a/debian/randao.timer b/debian/etc/systemd/system/randao.timer similarity index 100% rename from debian/randao.timer rename to debian/etc/systemd/system/randao.timer diff --git a/debian/etc/update-motd.d/66-randao b/debian/etc/update-motd.d/66-randao new file mode 100644 index 0000000..4a80554 --- /dev/null +++ b/debian/etc/update-motd.d/66-randao @@ -0,0 +1,59 @@ +#!/bin/bash +THIS_SCRIPT="randao" +MOTD_DISABLE="" + +[[ -f /etc/default/armbian-motd ]] && . /etc/default/armbian-motd + +for f in $MOTD_DISABLE; do + [[ $f == $THIS_SCRIPT ]] && exit 0 +done + +## FORMATTING VARIABLES +# --- ANSI Color Codes --- +RED=$'\e[31m' +GREEN=$'\e[32m' +BLUE=$'\e[94m' +YELLOW=$'\e[93m' +GOLD=$'\e[38;5;214m' +ORANGE=$'\e[38;5;208m' +MAGENTA=$'\e[35m' +CYAN=$'\e[36m' + +# if using color with effects, use color first and then the effect. The color codes above reset the effect to none. +BOLD=$'\e[1m' # bold +ITAL=$'\e[3m' # italics +ULINE=$'\e[4m' # underline +XOUT=$'\e[9m' # crossed out +REV=$'\e[7m' # reversed +NC=$'\e[0m' # No Color (resets to default) + + +#servicelist=("randao.service" "randao.timer") +servicelist=("randao.timer") + +for service in "${servicelist[@]}"; do + if [[ -f "/etc/systemd/system/${service}" ]]; then + serviceEnabled=$(systemctl is-enabled $service) + if [[ ${serviceEnabled} == "enabled" ]]; then + serviceEnabled="${BOLD}${GREEN}enabled${NC}" + serviceMessage="" + else + serviceEnabled="${BOLD}${YELLOW}disabled${NC}" + serviceMessage="To enable ${BOLD}${service}${NC}: sudo systemctl enable ${service}" + fi + + serviceActive=$(systemctl is-active $service) + if [[ ${serviceActive} == "active" ]]; then + # service is active + serviceActive="${BOLD}${GREEN}active${NC}" + serviceMessage+="" + else + # service is in-active + serviceActive="${BOLD}${YELLOW}inactive${NC}" + serviceMessage+="\nThen start ${BOLD}${service}${NC}: sudo systemctl start ${service}" + fi + + echo -e "${BOLD}${service} is $serviceEnabled and $serviceActive." + echo -e "${serviceMessage}\n" + fi +done diff --git a/debian/install b/debian/install index 2007181..ffaaef1 100644 --- a/debian/install +++ b/debian/install @@ -2,12 +2,13 @@ # Destination on target system (relative to /) # Systemd service and timer files -debian/randao.service /etc/systemd/system/ -debian/randao.timer /etc/systemd/system/ +debian/etc/systemd/system/randao.service /etc/systemd/system/ +debian/etc/systemd/system/randao.timer /etc/systemd/system/ +debian/etc/update-motd.d/66-randao /etc/update-motd.d/66-randao # Docker Compose project files -docker-compose/ /opt/randao-provider/ -orchestrator/ /opt/randao-provider/ +docker-compose/ /opt/randao-provider/ +orchestrator/ /opt/randao-provider/ puzzle-generator/ /opt/randao-provider/ requester/ /opt/randao-provider/ LICENSE /opt/randao-provider/ diff --git a/docker-compose/docker-compose.appliance.yml b/docker-compose/docker-compose.appliance.yml index c8060a2..10c20a8 100644 --- a/docker-compose/docker-compose.appliance.yml +++ b/docker-compose/docker-compose.appliance.yml @@ -1,30 +1,28 @@ -# /home/randao/RandaoProvider/docker-compose/docker-compose.appliance.yml -# This file contains overrides specific to the appliance deployment. -# It should be loaded AFTER the base docker-compose.yml. - +# /home/randao/Randomness-Provider.git/docker-compose/docker-compose.appliance.yml services: orchestrator: volumes: # Override wallet mounts to point to /etc/randao/ for appliance security - # This explicitly mounts wallet.json from /etc/randao/ - /etc/randao/wallet.json:/app/config/wallet.json:ro - # This explicitly mounts wallet.seed from /etc/randao/ (if used) - - /etc/randao/wallet.seed:/app/config/wallet.seed:ro - deploy: # Appliance-specific deploy rules + - /etc/randao/wallet.seed:/app/config/wallet.seed:ro # If used + deploy: # Appliance-specific orchestrator deploy rules resources: limits: - cpus: '2.0' # Allow orchestrator up to 1.5 cores + cpus: '1.5' # Allow orchestrator up to 1.5 cores memory: 300M # Allow up to 300MB RAM reservations: - cpus: '1.0' # Reserve 0.5 of a core + cpus: '0.5' # Reserve 0.5 of a core memory: 192M - postgres: # Appliance-specific deploy rules for Postgres + postgres: # Appliance-specific postgres deploy rules and config mount + volumes: + - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro # <--- ADDED HERE + shm_size: '64m' # <--- ADDED HERE (or adjust as needed) deploy: resources: limits: - cpus: '0.8' # Limit Postgres to 40% of one core + cpus: '0.4' # Limit Postgres to 40% of one core memory: 150M # Limit Postgres to 150MB RAM reservations: - cpus: '0.4' # Reserve 10% of one core + cpus: '0.1' # Reserve 10% of one core memory: 64M \ No newline at end of file diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 663c402..5f1ad04 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,7 +1,4 @@ # /home/randao/RandaoProvider/docker-compose/docker-compose.yml (Base) -# This file defines the core services and their common configuration. -# It uses relative paths for secrets, suitable for standalone/dev environments. - services: postgres: image: postgres:13-alpine @@ -15,7 +12,7 @@ services: - backend volumes: - pgdata:/var/lib/postgresql/data - - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro # Relative path for custom Postgres config + # REMOVED: - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-mydatabase}"] interval: 10s @@ -26,10 +23,15 @@ services: options: max-size: "100m" max-file: "5" - shm_size: '64m' + # REMOVED: shm_size: '64m' - Docker's default shm_size (64MB) will be used + # REMOVED: NO deploy section here; no device-specific limits in the base file. orchestrator: - image: randao/orchestrator:latest # Or your chosen stable tag (e.g., appliance-stable) + image: hottoddie/orchestrator:custom-file-wallet # Or your chosen stable tag (e.g., appliance-stable) + # build: + # context: ../orchestrator/ + # dockerfile: Dockerfile + pull_policy: always depends_on: postgres: condition: service_healthy @@ -39,24 +41,16 @@ services: DB_USER: ${DB_USER:-myuser} DB_PASSWORD: ${DB_PASSWORD:-mypassword} DB_NAME: ${DB_NAME:-mydatabase} - # New environment variables to point to the wallet files INSIDE the container - # The app code will prioritize these if the files are mounted and readable. - WALLET_JSON_FILE_PATH: /app/config/wallet.json # For JWK JSON wallet. container path after the volume is mounted (see 'volumes:' below) - SEED_FILE_PATH: /app/config/wallet.seed # For mnemonic seed phrase. container path after the volume is mounted (see 'volumes:' below) - # WALLET_JSON is passed. If WALLET_JSON_FILE_PATH fails/is missing, app falls back to this. + WALLET_JSON_FILE_PATH: /app/config/wallet.json + SEED_FILE_PATH: /app/config/wallet.seed WALLET_JSON: ${WALLET_JSON} - SEED_PHRASE: ${SEED_PHRASE} # Pass if you want to support seed phrase env var too DOCKER_NETWORK: backend networks: - backend volumes: - - /var/run/docker.sock:/var/run/docker.sock # Common Docker socket mount - # Mount wallet files from the project directory for standalone/dev - # If ./wallet.json doesn't exist, Docker will create an empty dir, - # but the app will fallback to WALLET_JSON env var if set. - # One of these is preferred for toaster/appliance approach (we could move them to the appliance yaml) - - ./wallet.json:/app/config/wallet.json:ro # Read-only mount for JWK file (optional, if used) - - ./wallet.seed:/app/config/wallet.seed:ro # Read-only mount for seed phrase file (optional, if used) + - /var/run/docker.sock:/var/run/docker.sock + - ./wallet.json:/app/config/wallet.json # Default/Standalone wallet.json mount + - ./wallet.seed:/app/config/wallet.seed # Default/Standalone wallet.seed mount logging: driver: json-file options: diff --git a/docker-compose/postgres/postgresql.conf b/docker-compose/postgres/postgresql.conf new file mode 100644 index 0000000..c0cb761 --- /dev/null +++ b/docker-compose/postgres/postgresql.conf @@ -0,0 +1,26 @@ +# /home/randao/RandomnProvider/docker-compose/postgres/postgresql.conf +# --- Memory --- +# Total available RAM is 512MB (0.5GB) +# Target for Postgres container is 150MB limit +shared_buffers = 40MB # ~25% of Postgres container's 150MB limit +work_mem = 1MB # For sorting/hashing per operation (start low) +maintenance_work_mem = 16MB # For VACUUM, CREATE INDEX (lower than default) +effective_cache_size = 60MB # Estimate of OS + shared_buffers cache (roughly 40% of container limit) +wal_buffers = 2MB # Smaller WAL buffers for low write loads + +# --- Connections --- +max_connections = 20 # Limit connections to reduce per-connection memory overhead + # Adjust based on orchestrator's needs. + +# --- Checkpointing --- +# Aim to reduce write spikes for low I/O systems +checkpoint_timeout = 10min # Increase from default 5min +max_wal_size = 256MB # Equivalent to ~16 checkpoint segments (16MB each) +checkpoint_completion_target = 0.9 # Spread out checkpoint writes more + +# --- Autovacuum --- +# Autovacuum can be resource-intensive; aggressive settings +# might be needed to keep table bloat down, but tune carefully. +# autovacuum_max_workers = 1 # Reduce workers +# autovacuum_vacuum_scale_factor = 0.05 # Vacuum more frequently on small tables +# autovacuum_analyze_scale_factor = 0.02 # Analyze more frequently diff --git a/docker-compose/wallet.json.example b/docker-compose/wallet.json.example new file mode 100644 index 0000000..72ca0b9 --- /dev/null +++ b/docker-compose/wallet.json.example @@ -0,0 +1,11 @@ +{ + "kty": "RSA", + "e": "AQAB", + "n": "zxP9Y4b1... (truncated for brevity)...eB8zP6Q", + "d": "Jk-9sR1b... (truncated for brevity)...k_1gQ2", + "p": "9h7x0zY0... (truncated for brevity)...wD4q", + "q": "8x6w9yZ8... (truncated for brevity)...vH9k", + "dp": "2o1p3q4r... (truncated for brevity)...sU5t", + "dq": "5t6u7v8w... (truncated for brevity)...xB9y", + "qi": "1a2b3c4d... (truncated for brevity)...gF0h" +} \ No newline at end of file diff --git a/docker-compose/wallet.seed.example b/docker-compose/wallet.seed.example new file mode 100644 index 0000000..f490c8b --- /dev/null +++ b/docker-compose/wallet.seed.example @@ -0,0 +1 @@ +example caution example caution example caution example caution example caution example caution \ No newline at end of file diff --git a/updates.md b/updates.md new file mode 100644 index 0000000..091960d --- /dev/null +++ b/updates.md @@ -0,0 +1,256 @@ +Here's the updated Markdown document, including an explanation of why using `wallet.json` or `wallet.seed` files is preferred. + +----- + +# Randao Provider Configuration Guide + +This document outlines how to deploy the Randao Provider application using Docker Compose, covering configurations for both standard user environments (e.g., Windows, macOS, Linux desktop) and dedicated Linux service/appliance environments. + +----- + +## 1\. Core Concepts & Files + +The Randao Provider deployment relies on these key files: + + * **`docker-compose.yml`**: The **base** Docker Compose file. It defines the core services (orchestrator, PostgreSQL), their dependencies, and common environment variables. It uses relative paths for `wallet.json` and `postgresql.conf` for portability and serves as the default configuration for standard user environments. + * **`.env`**: A plain text file storing environment variables (database credentials, network settings). This file is sourced by Docker Compose. + * **`wallet.json`**: Your Arweave wallet's JWK (JSON Web Key) file. This contains sensitive private key information. + * **`wallet.seed`** (Optional): If you use a mnemonic seed phrase instead of a JWK. + * **`docker-compose.appliance.yml`**: An **override** Docker Compose file specifically for appliance deployments. It defines absolute paths for sensitive files (like `wallet.json`) and appliance-specific resource limits. + * **`postgres/postgresql.conf`**: Custom PostgreSQL configuration for resource-constrained environments. + * **Wallet Management**: The application's source code (specifically `walletUtils.ts`) has been modified to prioritize reading wallet information securely from mounted files (e.g., `wallet.json` or `wallet.seed`), falling back to environment variables (`WALLET_JSON` or `SEED_PHRASE`) if files aren't found. This guide assumes you are using an image that includes these modifications. + +----- + +## 2\. Why Use Wallet Files Instead of Environment Variables? + +Using dedicated files (like `wallet.json` or `wallet.seed`) for sensitive wallet information is **strongly preferred for security reasons** over passing this data directly as environment variables (`WALLET_JSON` or `SEED_PHRASE`). + +Here's why: + + * **Reduced Visibility (Primary Reason):** + * **Environment Variables (`WALLET_JSON`, `SEED_PHRASE`):** These are notoriously insecure for sensitive data. Anyone with access to the Docker host (even a non-root user with `docker` group access) can easily inspect a running container's environment variables using the `docker inspect ` command. This means your full wallet private key could be displayed in plain text in the command's output. + * **Files (`wallet.json`, `wallet.seed`):** When you mount a file into a container (e.g., `/etc/randao/wallet.json` into `/app/config/wallet.json`), the file's content is not directly exposed as an environment variable of the running process. An attacker would need filesystem access to `/etc/randao/wallet.json` on the host (which can be protected with strict permissions), *and* potentially shell access *inside* the container, to read the file. + * **Principle of Least Privilege (Filesystem):** You can set very tight file permissions on the host (e.g., `chmod 640` or `600`) for `wallet.json` and `wallet.seed`. This allows only the necessary user (e.g., `root` for ownership, `randao_service` user for read access via group) to access the file, further limiting exposure. + * **Best Practice:** Mounting sensitive data as files is the industry-standard best practice for secret management in containerized environments (e.g., Docker Secrets in Swarm mode, Kubernetes Secrets mounted as volumes). + * **Logging:** Environment variables can sometimes inadvertently end up in logs if the application or logging system isn't carefully configured. File contents are less prone to this leakage. + +While the application supports falling back to environment variables for convenience, **using the file-based method for your wallet is always the more secure choice, especially for production or appliance deployments.** + +----- + +## 3\. Setting Up the Project Directory + +Begin by cloning the Randao Provider repository from GitHub and organizing your configuration files. + +### **3.1. Clone the Repository:** + +```bash +git clone https://github.com/RandAOLabs/Randomness-Provider.git your-randao-provider-repo +``` + +### **3.2. Navigate to the Docker Compose Directory:** + +```bash +cd your-randao-provider-repo/docker-compose/ +``` + +### **3.3. Project Directory Structure:** + +Your directory should look similar to this: + +``` +your-randao-provider-repo/ +├── docker-compose/ +│ ├── docker-compose.yml # Base Docker Compose file +│ ├── docker-compose.appliance.yml # Appliance-specific overrides +│ ├── postgres/ +│ │ └── postgresql.conf # Custom Postgres config +│ ├── .env.example # Example .env file (for users to copy) +│ ├── wallet.json.example # Example wallet.json (for users to copy) +│ └── wallet.seed.example # Example wallet.seed (optional) +├── orchestrator/ # Contains Dockerfile and walletUtils.ts (source, not used directly by docker compose up) +│ └── Dockerfile +│ └── src/walletUtils.ts +├── LICENSE +└── README.md +``` + +----- + +## 4\. Configuration for a Standard Docker User (e.g., Windows, macOS, Linux Desktop) + +This setup is for users who want to run the provider locally without systemd integration, using pre-built Docker images. + +### **4.1. Prerequisites:** + + * **Docker Desktop** (Windows/macOS) or **Docker Engine** (Linux) installed and running. + * Access to the command line/terminal. + +### **4.2. Setup Steps:** + +1. **Navigate to the `docker-compose` directory** (if not already there): + + ```bash + cd your-randao-provider-repo/docker-compose/ + ``` + +2. **Create `.env` file:** + Copy the example `.env` file and **fill in your database credentials**. + + ```bash + cp .env.example .env + # Open .env in a text editor and fill in DB_USER, DB_PASSWORD, DB_NAME, DOCKER_NETWORK, LOG_CONSOLE_LEVEL + # Example .env content: + # DB_USER=myuser + # DB_PASSWORD=mypassword + # DB_NAME=mydatabase + # DOCKER_NETWORK=backend + # LOG_CONSOLE_LEVEL=7 + ``` + +3. **Create `wallet.json` (or `wallet.seed`):** + Copy the example wallet file and **paste your actual Arweave wallet's JWK content** (or seed phrase) into it. + + ```bash + cp wallet.json.example wallet.json + # Open wallet.json in a text editor and paste your JWK content. + # On Linux/macOS, set permissions for security: + chmod 600 wallet.json + ``` + + * **Fallback Option:** If you prefer not to create `wallet.json` directly, you can put `WALLET_JSON='{"your_jwk_content"}'` directly into your `.env` file. The application code will fall back to this environment variable if it cannot read `wallet.json` from the mounted file. **Note that this fallback is less secure.** + +4. **Run the Docker Compose Stack:** + Use the base `docker-compose.yml`. `docker compose` will pull the necessary images. + + ```bash + docker compose up --pull=always + ``` + + * `--pull=always`: Ensures the latest image versions are pulled from Docker Hub. + * `up`: Starts the services in the foreground. Add `-d` to run in detached mode (background). + +5. **Monitor Logs:** + + ```bash + docker compose logs -f + ``` + +----- + +## 5\. Configuration for a Linux Service / Appliance + +This setup provides robust, automated management via `systemd`, enhanced security, and consistent updates, using pre-built Docker images. + +### **5.1. Prerequisites:** + + * **Debian/Ubuntu** (or similar Linux distribution) installed. + * **Docker Engine** and **Docker Compose V2** installed. + * **`randao_service` system user** created (e.g., `sudo adduser --system --no-create-home --group --uid 888 randao_service`). + * `randao_service` user added to the `docker` group (e.g., `sudo usermod -aG docker randao_service`). + * **Swap space** configured (highly recommended for low-RAM devices like H3). + * **Ownership and permissions** for the project directory set for `randao_service`. + ```bash + sudo chown -R randao_service:randao_service /home/randao/Randomness-Provider.git/ + sudo chmod -R u=rwX,go=rX /home/randao/Randomness-Provider.git/ + ``` + +### **5.2. Setup Steps:** + +1. **Place Sensitive Configuration Files in `/etc/randao/`:** + These files are managed by `root` but readable by `randao_service`. This is the **preferred and most secure location** for appliance secrets. + + ```bash + # Create the directory + sudo mkdir -p /etc/randao/ + + # Copy your actual .env and wallet.json files from your local setup or provisioning source + # Example (assuming they are temporarily available in /tmp/ during provisioning): + sudo cp /tmp/.env /etc/randao/.env + sudo cp /tmp/wallet.json /etc/randao/wallet.json + sudo cp /tmp/wallet.seed /etc/randao/wallet.seed # If using seed file + + # Set ownership and permissions for the directory + sudo chown root:root /etc/randao/ + sudo chmod 700 /etc/randao/ # Root only access to the directory itself + + # Set ownership and permissions for the files + sudo chown root:randao_service /etc/randao/.env + sudo chmod 640 /etc/randao/.env # Root R/W, randao_service group R, others no access + + sudo chown root:randao_service /etc/randao/wallet.json + sudo chmod 640 /etc/randao/wallet.json + + # If wallet.seed is used + sudo chown root:randao_service /etc/randao/wallet.seed + sudo chmod 640 /etc/randao/wallet.seed + ``` + +2. **Place `docker-compose` Project Files:** + Copy the cloned repository contents to a system location like `/home/randao/Randomness-Provider.git/`. + + ```bash + # Example: + sudo cp -r /path/to/your/cloned-repo/Randomness-Provider.git /home/randao/ + ``` + +3. **Create Systemd Service Unit (`randao.service`):** + Create `/etc/systemd/system/randao.service` with the following content: + + ```ini + # /etc/systemd/system/randao.service + [Unit] + Description=RANDAO Provider + Documentation=https://github.com/RandAOLabs/Randomness-Provider + Requires=docker.service + After=network-online.target docker.service + + [Service] + Type=simple + User=randao_service + Group=randao_service + WorkingDirectory=/home/randao/Randomness-Provider.git/docker-compose + ExecStart=/usr/bin/docker compose -f docker-compose.yml -f docker-compose.appliance.yml --env-file /etc/randao/.env up --pull=always + ExecStop=/usr/bin/docker compose down + TimeoutStartSec=0 + Restart=on-failure + RestartSec=5s + + [Install] + WantedBy=multi-user.target + ``` + +4. **Create Systemd Timer Unit (`randao.timer`):** + Create `/etc/systemd/system/randao.timer` with the following content for periodic updates: + + ```ini + # /etc/systemd/system/randao.timer + [Unit] + Description=Timer to periodically restart RANDAO Provider for latest image pull + + [Timer] + OnCalendar=*-*-* 00,12:00:00 # Restart every day at midnight and noon UTC + RandomizedDelaySec=30min # Add a random delay to prevent stampedes + Persistent=true # Trigger on boot if a scheduled run was missed + OnBootSec=10s # Start 10 seconds after system boot (initial run) + + [Install] + WantedBy=timers.target + ``` + +5. **Enable & Start Services:** + + ```bash + sudo systemctl daemon-reload # Reload systemd to recognize new units + sudo systemctl enable randao.timer # Enable the timer for autostart on reboot + sudo systemctl start randao.timer # Start the timer immediately + # The timer will then trigger randao.service (e.g., after 10 seconds due to OnBootSec) + ``` + +6. **Monitor Logs:** + + ```bash + sudo journalctl -u randao.service -f # Monitor real-time logs from your service + sudo systemctl status randao.timer # Check timer's status and next activation + ``` \ No newline at end of file From 1c9e982c0ab2983d929d5e225066acceb4fa6eec Mon Sep 17 00:00:00 2001 From: emerson Date: Wed, 23 Jul 2025 19:07:45 -0400 Subject: [PATCH 80/80] updates to multi-platform notes and quickstart --- docker-compose/QuickStart.md | 104 ++++++++++++++++++++++++++++++ docker-compose/docker-compose.yml | 3 - orchestrator/docs/todd-builder.md | 37 +++++++++++ updates.md => todd-updates.md | 4 -- 4 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 docker-compose/QuickStart.md create mode 100644 orchestrator/docs/todd-builder.md rename updates.md => todd-updates.md (98%) diff --git a/docker-compose/QuickStart.md b/docker-compose/QuickStart.md new file mode 100644 index 0000000..e2b9ead --- /dev/null +++ b/docker-compose/QuickStart.md @@ -0,0 +1,104 @@ +# **Randao Provider: Quick Start Guide** + +This guide provides a quick way to get the Randao Provider running on your local machine using Docker Compose. This is ideal for development, testing, or non-appliance deployments. +**Assumptions:** + +- You have **Docker Desktop** (Windows/macOS) or **Docker Engine** (Linux) installed and running. +- You have basic command-line knowledge. +- You are using the official randao/orchestrator image, which has been pre-built with the necessary wallet management modifications. +- **You have a compatible Arweave wallet (JWK file or mnemonic seed phrase) ready.** + +## **1\. Get the Project Files** + +First, clone the Randao Provider repository from GitHub: +```sh +git clone https://github.com/RandAOLabs/Randomness-Provider.git randao-provider +``` + +Now, navigate into the Docker Compose directory: +``sh +cd randao-provider/docker-compose/ +`` + +## **2\. Prepare Configuration Files** + +You need to create/edit two essential configuration files: .env (for database and logging) and your wallet key file (wallet.json or wallet.seed). + +### **2.1. Create .env (Environment Variables)** + +This file stores your database credentials and other settings. + +1. Copy the example .env file: + cp .env.example .env + +2. Open the newly created .env file in a text editor (e.g., nano .env or code .env) and **fill in your desired values** for the database user, password, and name. You can keep the defaults if running locally for testing. + \# .env + DB\_USER=myuser + DB\_PASSWORD=mypassword + DB\_NAME=mydatabase + DOCKER\_NETWORK=backend + LOG\_CONSOLE\_LEVEL=3 \# Set to 7 for verbose (DEBUG) logs + + +### **2.2. Create Wallet Key File (wallet.json or wallet.seed)** + +This file contains your Arweave wallet's private key (JWK) or mnemonic seed phrase. The application will prioritize reading from wallet.json (JWK) if both are present. If neither file is found, it will fall back to environment variables. We recommend using Wander as the Chrome plugin integrates easily with our [Provider Portal](https://providers_randao.ar.io/providers). + +#### **Option A: Using wallet.json (JWK)** + +1. Copy the example wallet.json file: + cp wallet.json.example wallet.json + +2. Open wallet.json in a text editor and **replace its content with your actual Arweave wallet's JWK (JSON Web Key) data.** + **⚠️ IMPORTANT SECURITY WARNING ⚠️** + + - **NEVER use the example wallet content for a real wallet.** Always generate your own unique Arweave wallet. + - **Keep your wallet.json file secure.** Do not share it or commit it to public repositories. + - On Linux/macOS, it is highly recommended to set strict permissions: + chmod 600 wallet.json + +#### **Option B: Using wallet.seed (Mnemonic Seed Phrase)** + +1. Copy the example wallet.seed file: + cp wallet.seed.example wallet.seed + +2. Open wallet.seed in a text editor and **replace its content with your actual Arweave wallet's mnemonic seed phrase.** The seed phrase must be 12, 18, or 24 words, separated by single spaces, with no extra characters. + **⚠️ IMPORTANT SECURITY WARNING ⚠️** + + - **NEVER use the example seed phrase for a real wallet.** Always generate your own unique Arweave wallet. + - **Keep your wallet.seed file secure.** Do not share it or commit it to public repositories. + - On Linux/macOS, it is highly recommended to set strict permissions: + chmod 600 wallet.seed + +### **2.3. Alternative (Less Secure Fallback): Environment Variables** + +If you prefer not to create wallet.json or wallet.seed files, you can instead add the wallet content directly into your .env file using the WALLET\_JSON or SEED\_PHRASE environment variables. The application will fall back to these if it cannot read from the mounted files. + +- **For JWK:** Add WALLET\_JSON='{"your\_jwk\_content\_here"}' to your .env file. +- **For Seed Phrase:** Add SEED\_PHRASE="your seed phrase words here" to your .env file. + +However, **this method is less secure as environment variables are easily inspectable.** + +## **3\. Run the Randao Provider** + +Now you can start your Docker Compose stack. This command will automatically pull the necessary Docker images and set up your services. +docker compose up \-d \--pull=always + +- \--pull=always: Ensures that Docker always checks for and pulls the latest versions of the images from Docker Hub. +- up: Starts the services in the foreground, showing their logs directly in your terminal. +- \-d: run the container in the background (detached mode) + + +## **4\. Monitor Logs** + +To see the real-time output from your running services (especially for debugging wallet initialization): + +`docker compose logs \-f` + +## **5\. Stop the Randao Provider** + +To stop and remove the running containers, networks, and volumes (excluding named volumes like pgdata): + +`docker compose down` + +You should now have your Randao Provider up and running locally\! \ No newline at end of file diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 5f1ad04..b3c35bd 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -28,9 +28,6 @@ services: orchestrator: image: hottoddie/orchestrator:custom-file-wallet # Or your chosen stable tag (e.g., appliance-stable) - # build: - # context: ../orchestrator/ - # dockerfile: Dockerfile pull_policy: always depends_on: postgres: diff --git a/orchestrator/docs/todd-builder.md b/orchestrator/docs/todd-builder.md new file mode 100644 index 0000000..8f7c4ab --- /dev/null +++ b/orchestrator/docs/todd-builder.md @@ -0,0 +1,37 @@ + +# Navigate to your Docker Compose project directory (on your amd64 machine) +cd /path/to/your/local/RandaoProvider/docker-compose/ + +# Export version as an environment variable +export VERSION=v1.0.12 # You can change this value to any version you want + +# Build the Docker image with the version tag +# docker build -t hottoddie/orchestrator:custom-file-wallet -t hottoddie/orchestrator:$VERSION . + +# Log in to Docker +docker login + +# Push the image with the version tag +docker push randao/orchestrator:latest +docker push randao/orchestrator:$VERSION + +# Create and use buildx builder +docker buildx create --use +docker buildx inspect --bootstrap + + + + + +# Build for multiple platforms (including amd64 for testing and arm64 for Orange Pi Zero 3) + + docker buildx build \ + --platform linux/amd64,linux/arm64,linux/arm/v7 \ + -t hottoddie/orchestrator:custom-file-wallet \ + --push \ + -f ../orchestrator/Dockerfile \ + ../orchestrator/ \ No newline at end of file diff --git a/updates.md b/todd-updates.md similarity index 98% rename from updates.md rename to todd-updates.md index 091960d..1722644 100644 --- a/updates.md +++ b/todd-updates.md @@ -1,7 +1,3 @@ -Here's the updated Markdown document, including an explanation of why using `wallet.json` or `wallet.seed` files is preferred. - ------ - # Randao Provider Configuration Guide This document outlines how to deploy the Randao Provider application using Docker Compose, covering configurations for both standard user environments (e.g., Windows, macOS, Linux desktop) and dedicated Linux service/appliance environments.