diff --git a/.gitignore b/.gitignore index eba74f4..2b9af51 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,56 @@ -venv/ \ No newline at end of file +# Ignore virtual environments +venv/ +.env/ +.env +env +*wallet.json +*wallet*.json +mass-docker-compose.yml +# Ignore distribution/build directories +dist/ +build/ +*.egg-info/ +*.pyc +__pycache__/ +pgdata/ + +# Ignore Node.js dependencies +node_modules/ +**/node_modules/ + +# Ignore Terraform and related files +*.tfstate +*.tfstate.backup +*.exe +*.lock.* +LICENSE.txt +terraform.tfvars + + +# 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/README.md b/README.md index e69de29..1f3cd0a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,235 @@ +# Node Provider Setup Guide + +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. [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) + +--- + +## Quickstart Guide + +Setting up your randomness provider is easy! Just follow these simple steps: + +### 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 + +### Step 3: Start Your Provider +Run this command to start your provider: +``` +docker-compose up -d +``` + +### Step 4: Stake Your Node +1. Navigate to ar://randao +2. Connect your wallet +3. Follow the staking instructions to activate your provider + +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. + +--- + +## 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 + +The better your provider performs these functions, the more rewards you'll receive. Providers with faster response times earn more! + +--- + +## Hardware Requirements + +To run a node, you'll need: +- At least 4 GB memory +- At least 2 CPU cores +- Reliable internet connection + +**Note:** These requirements may increase over time as the network grows. + +--- + +## 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 + +By completing this process, you will fully activate your node and ensure it is ready for network participation. + +--- + +## Maintenance + +To update your provider when new versions are released: + +```bash +docker-compose pull +docker-compose down +docker-compose up -d +``` + +Remember to follow the graceful shutdown procedure when performing maintenance to avoid 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: + +1. Go to ar://randao +2. Navigate to your node +3. Select the "SHUT DOWN" button and sign the transaction +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! + +--- + +## 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 + +### 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/docker-compose/.env.example b/docker-compose/.env.example new file mode 100644 index 0000000..512557e --- /dev/null +++ b/docker-compose/.env.example @@ -0,0 +1,4 @@ +DB_USER=myuser +DB_PASSWORD=mypassword +DB_NAME=mydatabase +DOCKER_NETWORK=backend diff --git a/docker-compose/README.md b/docker-compose/README.md new file mode 100644 index 0000000..fa60c4a --- /dev/null +++ b/docker-compose/README.md @@ -0,0 +1,128 @@ +# Docker Compose Setup Guide + +This guide walks you through deploying a randomness provider using Docker Compose on your own hardware. + +## 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 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 + ``` + +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 + +## 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. + +To update your provider when new versions are released: + +```bash +docker-compose pull +docker-compose down +docker-compose up -d +``` + +--- + +[Return to Main Documentation](../README.md) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml new file mode 100644 index 0000000..da14667 --- /dev/null +++ b/docker-compose/docker-compose.yml @@ -0,0 +1,46 @@ +services: + postgres: + image: postgres:13-alpine + environment: + POSTGRES_USER: ${DB_USER:-myuser} + POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword} + POSTGRES_DB: ${DB_NAME:-mydatabase} + ports: + - "5431: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.4.68 + 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} + PATH_TO_WALLET: /app/wallet.json # Path inside the container + 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/docs/become-a-provider.md b/docs/become-a-provider.md deleted file mode 100644 index e69de29..0000000 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 new file mode 100644 index 0000000..3bdc53a --- /dev/null +++ b/orchestrator/Dockerfile @@ -0,0 +1,23 @@ +# Use the official lightweight Node.js image +FROM node:22-bullseye-slim + +# Create a working directory +WORKDIR /usr/src/app + +# Copy package files first to leverage Docker layer caching +COPY package*.json ./ + +# Install dependencies including dev dependencies (TypeScript) +RUN npm install + +# Copy the source files +COPY . . + +# Compile TypeScript code +RUN npx tsc + +# Expose the app port +EXPOSE 3000 + +# Run the app +CMD ["node", "dist/app.js"] diff --git a/orchestrator/README.md b/orchestrator/README.md index e69de29..5470a55 100644 --- a/orchestrator/README.md +++ b/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/orchestrator/docs/development.md b/orchestrator/docs/development.md index e69de29..d4a3f0a 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -0,0 +1,39 @@ +To build: + +Save all files +Run: +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 + + + + + + + + +# Export version as an environment variable +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 . + +# 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 the multi-platform image and push it +docker buildx build --platform linux/amd64,linux/arm64 \ +-t randao/orchestrator:latest \ +-t randao/orchestrator:$VERSION \ +--push . diff --git a/orchestrator/package.json b/orchestrator/package.json new file mode 100644 index 0000000..fdbc234 --- /dev/null +++ b/orchestrator/package.json @@ -0,0 +1,29 @@ +{ + "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": "^6.0.18", + "ao-vrf": "file:", + "arweave": "^1.15.5", + "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..4c5720a --- /dev/null +++ b/orchestrator/src/app.ts @@ -0,0 +1,164 @@ +import { readFile } from 'node:fs/promises'; +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 {monitorDockerContainers } from './containerManagment.js'; + +if (!process.env.PATH_TO_WALLET) { + console.error("Env var PATH_TO_WALLET is not set!"); + process.exit(1); +} + +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 +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; + + +const arweave = Arweave.init({}); + +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 }, + }; +} + +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 + return `[LogID: ${randomId} | ${timestamp}]`; +} + +async function polling(client: any) { + if (pollingInProgress) { + 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 + + // 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 + await Promise.all([ + (async () => { + const s2 = Date.now(); + console.log(`${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`); + })(), + (async () => { + const s3 = Date.now(); + console.log(`${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`); + })(), + (async () => { + const s4 = Date.now(); + 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`); + })(), + ]); + + const totalTime = Date.now() - startTime; + console.log(`${logId} Polling cycle completed successfully. Total time taken: ${totalTime}ms`); + + } catch (error) { + console.error(`${logId} An error occurred during polling:`, error); + } finally { + pollingInProgress = false; // Reset flag after execution + } +} + + + +// Main function +async function run(): Promise { + const client = await connectWithRetry(); + await setupDatabase(client); + + const providerAddress = arweave.wallets.jwkToAddress(JSON.parse(await readFile(process.env.PATH_TO_WALLET!, 'utf8'))); + console.log('Provider address:', providerAddress); + + // 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(); + }, DOCKER_MONITORING_TIME); // Cleanup every 30 seconds + + setInterval(async () => { + await polling(client); + }, POLLING_INTERVAL_MS); + + process.on("SIGTERM", async () => { + console.log("SIGTERM received. Closing database connection."); + await client.end(); + await shutdown(); + process.exit(0); + }); +} + +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..a9ac742 --- /dev/null +++ b/orchestrator/src/containerManagment.ts @@ -0,0 +1,243 @@ +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 { + 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)}`; + + if (ongoingContainers.size > 0) { + console.log("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; + } + } + + 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; + } +} + +export async function getMoreRandom(currentCount: number) { + const entriesNeeded = MINIMUM_ENTRIES - currentCount; + console.log(`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; + } + + 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; + 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'; +} 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/helperFunctions.ts b/orchestrator/src/helperFunctions.ts new file mode 100644 index 0000000..21def45 --- /dev/null +++ b/orchestrator/src/helperFunctions.ts @@ -0,0 +1,475 @@ +import { readFile } from 'node:fs/promises'; +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, Logger, LogLevel, 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"; + + +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; +}, 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(await readFile(process.env.PATH_TO_WALLET!, 'utf8'))) + .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 { + 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; + } +} + + +// 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 + + // Check if more entries are needed + if (currentCount >= MINIMUM_ENTRIES) return; + getMoreRandom(currentCount) + ongoingRequest = true; + + } catch (error) { + console.error('Error during check and fetch:', error); + } finally { + ongoingRequest = false; // Allow future operations + } +} + +export function updateAvailableValuesAsync(currentCount: number) { + return (async () => { + try { + await (await getRandomClient()).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 { //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; + } +} + +// 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); + } +} + +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/orchestrator/src/oldjunk/clear_outputs.tzs b/orchestrator/src/oldjunk/clear_outputs.tzs new file mode 100644 index 0000000..88772fc --- /dev/null +++ b/orchestrator/src/oldjunk/clear_outputs.tzs @@ -0,0 +1,65 @@ +import { Client } from 'pg'; +import { RandomClient, RandomClientConfig } from "ao-process-clients"; +import { dbConfig } from './db_config'; + +// Random Client Configuration +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 +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 (await getRandomClient()).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 (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); + } + }); + + 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/oldjunk/stake.tzs b/orchestrator/src/oldjunk/stake.tzs new file mode 100644 index 0000000..862b81c --- /dev/null +++ b/orchestrator/src/oldjunk/stake.tzs @@ -0,0 +1,51 @@ +import { ProviderDetails, ProviderStakingClient, StakingClientConfig } from "ao-process-clients"; + +// Random Client Configuration +// 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!), + tokenProcessId: '5ZR9uegKoEhE9fJMbs-MvWLIztMNCVxgpzfeBVE3vqI', + processId: 'EIQJoqVWonlxsEe8xGpQZhh54wrmgE3q0tAsVIhKYQU' +} +const randclient = new ProviderStakingClient(RANDOM_CONFIG) + return randclient +} + + +// 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 (await getStakingClient()).stakeWithDetails("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/orchestrator/src/reset_db.ts b/orchestrator/src/reset_db.ts new file mode 100644 index 0000000..8a58e79 --- /dev/null +++ b/orchestrator/src/reset_db.ts @@ -0,0 +1,50 @@ +import { Client } from 'pg'; +import { dbConfig } from './db_tools'; + +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 new file mode 100644 index 0000000..bb6eb3a --- /dev/null +++ b/orchestrator/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "commonjs", + "target": "es6", + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": 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_tools.ts", + "src/clear_all_output_requests.ts", + "src/reset_db.mjs", + "src/clear_outputs.tzs" + ], + "exclude": [ + "node_modules" + ] +} 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/puzzle-generator/.pylintrc b/puzzle-generator/.pylintrc new file mode 100644 index 0000000..4b2ad19 --- /dev/null +++ b/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/puzzle-generator/Dockerfile b/puzzle-generator/Dockerfile new file mode 100644 index 0000000..be0ffdf --- /dev/null +++ b/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/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 53% rename from verifiable-delay-function/docs/developing.md rename to puzzle-generator/docs/developing.md index 6322f61..6a6291b 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: @@ -50,4 +52,32 @@ pytest With coverage: ```bash pytest --cov=src -``` \ No newline at end of file +``` + + + + + + +# 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/puzzle-generator/main.py b/puzzle-generator/main.py new file mode 100644 index 0000000..a7d10d7 --- /dev/null +++ b/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/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/puzzle-generator/src/converters/__init__.py b/puzzle-generator/src/converters/__init__.py new file mode 100644 index 0000000..ee1ed54 --- /dev/null +++ b/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/puzzle-generator/src/converters/rsa_converter.py b/puzzle-generator/src/converters/rsa_converter.py new file mode 100644 index 0000000..209308c --- /dev/null +++ b/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/puzzle-generator/src/converters/time_lock_puzzle_converter.py b/puzzle-generator/src/converters/time_lock_puzzle_converter.py new file mode 100644 index 0000000..4eb498d --- /dev/null +++ b/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/puzzle-generator/src/database/DatabaseService.py b/puzzle-generator/src/database/DatabaseService.py new file mode 100644 index 0000000..0df181e --- /dev/null +++ b/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/verifiable-delay-function/src/converters/__init__.py b/puzzle-generator/src/database/__init__.py similarity index 100% rename from verifiable-delay-function/src/converters/__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/puzzle-generator/src/database/entity/RSAEntity.py b/puzzle-generator/src/database/entity/RSAEntity.py new file mode 100644 index 0000000..7ddb16f --- /dev/null +++ b/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/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py b/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py new file mode 100644 index 0000000..487f5db --- /dev/null +++ b/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/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/__init__.py b/puzzle-generator/src/database/mixins/__init__.py similarity index 100% rename from verifiable-delay-function/src/database/__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/puzzle-generator/src/mpc/MPC.py b/puzzle-generator/src/mpc/MPC.py new file mode 100644 index 0000000..169c273 --- /dev/null +++ b/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/puzzle-generator/src/mpc/__init__.py b/puzzle-generator/src/mpc/__init__.py new file mode 100644 index 0000000..ab216aa --- /dev/null +++ b/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/puzzle-generator/src/mpc/abstract/IMPC.py b/puzzle-generator/src/mpc/abstract/IMPC.py new file mode 100644 index 0000000..4912662 --- /dev/null +++ b/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/puzzle-generator/src/mpc/abstract/__init__.py b/puzzle-generator/src/mpc/abstract/__init__.py new file mode 100644 index 0000000..557827b --- /dev/null +++ b/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/puzzle-generator/src/mpc/types.py b/puzzle-generator/src/mpc/types.py new file mode 100644 index 0000000..b9497fd --- /dev/null +++ b/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/puzzle-generator/src/primes/Primes.py b/puzzle-generator/src/primes/Primes.py new file mode 100644 index 0000000..6615545 --- /dev/null +++ b/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/puzzle-generator/src/primes/__init__.py b/puzzle-generator/src/primes/__init__.py new file mode 100644 index 0000000..52cdf79 --- /dev/null +++ b/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/puzzle-generator/src/primes/abstract/IPrimes.py b/puzzle-generator/src/primes/abstract/IPrimes.py new file mode 100644 index 0000000..2d49682 --- /dev/null +++ b/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/puzzle-generator/src/primes/abstract/__init__.py b/puzzle-generator/src/primes/abstract/__init__.py new file mode 100644 index 0000000..0ee899c --- /dev/null +++ b/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/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/puzzle-generator/src/random/Random.py b/puzzle-generator/src/random/Random.py new file mode 100644 index 0000000..d5e1458 --- /dev/null +++ b/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/puzzle-generator/src/random/__init__.py b/puzzle-generator/src/random/__init__.py new file mode 100644 index 0000000..3c8b236 --- /dev/null +++ b/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/puzzle-generator/src/random/abstract/IRandom.py b/puzzle-generator/src/random/abstract/IRandom.py new file mode 100644 index 0000000..2e6a0ce --- /dev/null +++ b/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/puzzle-generator/src/random/abstract/__init__.py b/puzzle-generator/src/random/abstract/__init__.py new file mode 100644 index 0000000..b22a9fd --- /dev/null +++ b/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/puzzle-generator/src/rsa/RSA.py b/puzzle-generator/src/rsa/RSA.py new file mode 100644 index 0000000..45b5bae --- /dev/null +++ b/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/puzzle-generator/src/rsa/__init__.py b/puzzle-generator/src/rsa/__init__.py new file mode 100644 index 0000000..4ac513e --- /dev/null +++ b/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/puzzle-generator/src/rsa/abstract/IRSA.py b/puzzle-generator/src/rsa/abstract/IRSA.py new file mode 100644 index 0000000..9897017 --- /dev/null +++ b/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/verifiable-delay-function/src/database/entity/__init__.py b/puzzle-generator/src/rsa/abstract/__init__.py similarity index 100% rename from verifiable-delay-function/src/database/entity/__init__.py rename to puzzle-generator/src/rsa/abstract/__init__.py diff --git a/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py new file mode 100644 index 0000000..996909f --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py new file mode 100644 index 0000000..0144200 --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py new file mode 100644 index 0000000..65a0a51 --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py new file mode 100644 index 0000000..be6e9da --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py new file mode 100644 index 0000000..19380a2 --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/__init__.py b/puzzle-generator/src/time_lock_puzzle/__init__.py new file mode 100644 index 0000000..33481c8 --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py new file mode 100644 index 0000000..f4b66df --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py b/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py new file mode 100644 index 0000000..c8f6889 --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py b/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py new file mode 100644 index 0000000..74b748d --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py b/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py new file mode 100644 index 0000000..aa24405 --- /dev/null +++ b/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/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py b/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py new file mode 100644 index 0000000..26bb910 --- /dev/null +++ b/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/verifiable-delay-function/src/database/mixins/__init__.py b/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py similarity index 100% rename from verifiable-delay-function/src/database/mixins/__init__.py rename to puzzle-generator/src/time_lock_puzzle/abstract/__init__.py diff --git a/puzzle-generator/src/time_lock_puzzle/constants.py b/puzzle-generator/src/time_lock_puzzle/constants.py new file mode 100644 index 0000000..acfd082 --- /dev/null +++ b/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/.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..412c1fb --- /dev/null +++ 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.4.5 . \ No newline at end of file diff --git a/requester/package.json b/requester/package.json new file mode 100644 index 0000000..1cda5f8 --- /dev/null +++ b/requester/package.json @@ -0,0 +1,29 @@ +{ + "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": { + "@permaweb/aoconnect": "^0.0.78", + "ao-process-clients": "^6.0.18", + "ao-vrf": "file:", + "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/requester/src/app.ts b/requester/src/app.ts new file mode 100644 index 0000000..bff7611 --- /dev/null +++ b/requester/src/app.ts @@ -0,0 +1,194 @@ +import { + RandomClient, +} from "ao-process-clients"; +import { TransferToProviders } from "./extra"; + +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; + +let cachedProviders: string[] = []; +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", +// }; + +let randomClientInstance: RandomClient | null = null; + +async function getRandomClient(): Promise { + + if (!randomClientInstance) { + randomClientInstance = ((await RandomClient.defaultBuilder())) + //.withAOConfig(AO_CONFIG) + .withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)) + .build(); + } + return randomClientInstance; +} + + + + +let totalRandomCalled = 0; +let totalTimeToFulfill = 0; +let fulfilledRequests = 0; +const outstandingRequests: Set = new Set(); + +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 + 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 randclient.getAllProviderActivity(); + console.log(providerInfo) + const eligibleProviders = providerInfo + //@ts-ignore + .filter(provider => provider.active === 1) + //@ts-ignore + .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 = await getRandomClient() + //const stakeclient = ProviderStakingClient.autoConfiguration(); + + 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 } = await getRandomProviders(randclient); + console.log(`Selected ${count} providers:`, providers); + //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..."); + } + + // // 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(); diff --git a/requester/src/extra.ts b/requester/src/extra.ts new file mode 100644 index 0000000..16547fe --- /dev/null +++ b/requester/src/extra.ts @@ -0,0 +1,64 @@ +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 = "ZBSQD_GeGUdQAiixxKy9Ag1rgJvJ_yFUGExwjW6mA7E" +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/tsconfig.json b/requester/tsconfig.json new file mode 100644 index 0000000..50dc9ee --- /dev/null +++ b/requester/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "commonjs", + "target": "es6", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, // ✅ Added to skip library type checking + "typeRoots": ["./node_modules/@types"] // ✅ Added to force correct type resolution + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file 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 6d061be..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 3a378fa..0000000 --- a/verifiable-delay-function/src/converters/verifiable_delay_function_converter.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import List -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/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) 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