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..87e08f5 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,354 @@ +# 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 [Frequently Asked Questions](#frequently-asked-questions) below. + +--- + +## How It Works + +Your provider performs 3 main functions: +1. It creates and stores crytographic time lock puzzles to be used for random entropy generation +2. It responds with the puzzle, then the answer when someone requests a random value +3. It keeps the AO proccess informed on its random values and status so users can see if its availible or not + +The better your provider performs these functions, the more rewards you'll receive. Providers with faster response times earn more! + +--- + +## Hardware Requirements + +To run a node, you'll need: +- At least 4 GB memory +- At least 2 CPU cores +- Reliable internet connection + + +--- + +## 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:** + - `local_db_user`: Database username (optional to change) + - `local_db_password`: Database password (optional to change) + - `db_name`: Database name (optional to change) + - `LOG_CONSOLE_LEVEL`: The log level you want to see + - `local_wallet_json`: Your wallet information + +2. **Deploy Your Provider** + From the Docker Compose directory, run: + ```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! + +--- + + +## Frequently Asked Questions + +### What is a randomness provider? +A provider generates **verifiable random numbers** for decentralized applications using a commit-reveal mechanism with timelock puzzles. These random values are essential for fairness in blockchain-based systems. + +### How do I earn rewards? +Once staked and online, you earn RNG-Test tokens based on: +- Your uptime +- Response speed +- Reliability in puzzle submission + +> ⚠️ You **must stake RNG-Test tokens** through the web UI and keep your node online to earn rewards. + +### How do I set up a provider? +1. Clone the repo. +2. Create a new wallet in your browser and send it RNG-Test tokens. +3. Stake using the site UI. +4. Place the wallet's **private key JSON** into your `.env` file. +5. Run `docker-compose up` from the repo directory. + +Your provider will start serving randomness as soon as it's recognized. + +### What are the minimum system requirements? +Minimum: +- 2 CPU cores +- 4 GB RAM +- Stable internet connection +- SSD or fast flash storage (not HDD) + +Recommended: +- 4 CPU cores +- 8 GB RAM + +Network stability and uptime matter more than CPU performance. + +### Can I run this on a Raspberry Pi? +Yes. A Raspberry Pi 4 or 5 works great. This service is very lightweight, and Pis are ideal for 24/7 uptime with low power usage. + +> 💡 Use fast external storage (USB SSD) if possible, and make sure your internet is reliable. + +### I rebooted and now my local and on-chain values don’t match. Is that okay? +Yes. This is normal — local and on-chain values can differ slightly due to how often each updates. As long as: +- There are **no errors** +- The on-chain “Available Random” is **positive** + +You’re fine. The system will reconcile automatically over time. + +### My `.env` file might be broken — how do I check it? +Check the following: +- The file matches `.env.example` +- You correctly pasted your **wallet's private key JSON** +- There are no formatting issues (e.g. missing quotes or equals signs) + +If unsure, restart your node with: +```bash +docker-compose down +docker-compose up +``` + +If it still fails, reach out in Discord. + +### I staked tokens but I get “Failed to stake tokens.” What should I do? +- Create a fresh wallet using the browser interface +- Send it RNG-Test tokens +- Stake via the site UI (you should see your balance) +- Paste the wallet's private key JSON into `.env` +- Restart the node + +If it still fails after confirming the above, open a support ticket in Discord. + +### My node is running but DB size, on-chain, and local values are all 0. What’s wrong? +Most likely causes: +- `.env` is misconfigured or contains an invalid wallet JSON +- The puzzle generator Docker image failed to pull + +Try: +- Checking `.env` for typos +- Pulling the image manually: `docker pull randao/puzzle-gen:v0.1.1` +- Restarting with `docker-compose down && docker-compose up` + +### My node is running, but the site doesn’t recognize me as a provider. What’s missing? +Two things must happen: +1. You must **stake** using the browser wallet +2. The `.env` file must contain the exact wallet JSON used for staking + +If either of these is missing or mismatched, the AO network won’t register you as an active provider. + +### Port 3000 is already in use — can I change it? +No need. The provider runs inside a **Docker virtual network** using port 3000 internally. It won't conflict with other services on your host machine, even if they use port 3000. + +> Your host system and other apps will not be affected. + +### Getting error: `No such image: randao/puzzle-gen:v0.1.1` — how do I fix this? +Run this manually: +```bash +docker pull randao/puzzle-gen:v0.1.1 +``` +This will fetch the image in case there was a permissions issue or the auto-pull failed. + +Once done, restart with `docker-compose up`. + +### Can I run this node on the same VPS as my Ar.io Gateway node? +Yes — this is a great combo. There are **no known conflicts** when running them together. The two services don’t compete for ports or storage and run happily side-by-side. + +> We are working on adding this provider as an optional Ar.io sidecar soon. + +### My node shut down and “Random Available” shows -2. What does that mean? +Negative values mean your provider is **offline** or **disabled**: +- `-1`: You manually shut it down +- `-2`: AO disabled you for being too slow +- `-3`: Disabled by the team (rare) + +Your provider won’t auto-recover. Go to the provider site and **toggle it back on manually**. + +### What does “Random Available” mean? +- **Positive number**: You’re active and have this much randomness ready to serve +- **0**: You shut down gracefully +- **Negative number**: You’ve been disabled (see above) + +Random must be generated **in advance** via timelock puzzles. That’s what’s reflected in this value. + +### What are the minimum requirements to run the node smoothly? +You need: +- **Minimum**: 2 CPU cores, 4 GB RAM +- **Recommended**: 4 CPU cores, 8 GB RAM +- **Storage**: SSD or fast flash storage only (no HDDs) + +The main requirement is **stable internet** and **high uptime**, not processing power. + +### How many tokens do I need to become a full validator? +You need **10,000 RNG-Test tokens** to become a validator. These can be claimed cheaply from the faucet. + +> Holding tokens alone does not qualify for airdrops — you must provide randomness (i.e. increase your served count). + +### What happens if my provider goes offline? +- If you shut it down **gracefully** (SIGTERM or UI), it sets `availableRandom` to 0 and avoids slashing. +- If you shut it down **abruptly**, you may be slashed or marked inactive. + +Always use the proper shutdown button or `CTRL+C` so the system knows you're offline safely. + +### Can I run multiple providers? +Yes. Each provider: +- Needs a **unique wallet** +- Must be **staked separately** +- Requires its own `.env` file and Docker instance + +You can scale across multiple servers or devices. + +### Do I need technical knowledge to run a provider? +Not much. If you can: +- Copy and paste into a terminal +- Edit a `.env` file +- Run Docker + +...you’re good to go. The setup is beginner-friendly and we offer full support via Discord. + +### I'm seeing a "Provider Not Found" error. What should I do? +- Make sure your wallet is **staked** via the provider site: `ar://randao_providers` +- Wait for the blockchain to confirm your stake (this may take a few minutes) +- Check that your `.env` file contains the **correct wallet private key JSON** + +### My provider can't connect to the network. How can I fix this? +- Check your **internet connection** +- Make sure your **firewall isn't blocking** outgoing connections or Docker traffic +- If the issue is due to network instability, **wait and try restarting later** + +### My provider is slow or unresponsive. What’s going on? +- Confirm your host has **enough CPU and memory** +- Check real-time logs: + ```bash + docker-compose logs -f + ``` +- If the puzzle generator is stalling, consider upgrading your hardware or verifying Docker is pulling the right image + +### I’m having general issues. How can I restart or reset? +First, try a clean restart: +```bash +docker-compose restart +``` + +If problems persist, do a full reset: +```bash +docker-compose down +docker-compose up -d +``` + +To update to the latest version: +```bash +docker-compose pull +docker-compose down +docker-compose up -d +``` + +### My database container won’t start. How do I debug this? +- Check logs: + ```bash + docker-compose logs db + ``` +- Make sure your `.env` has a database password that **doesn’t contain special characters** (some need escaping) +- If using Linux, **check the volume permissions** on your database folder diff --git a/docs/become-a-provider.md b/debian/changelog similarity index 100% rename from docs/become-a-provider.md rename to debian/changelog diff --git a/debian/conffiles b/debian/conffiles new file mode 100644 index 0000000..d567f72 --- /dev/null +++ b/debian/conffiles @@ -0,0 +1,3 @@ +/etc/randao/.env +/etc/randao/wallet.json +/etc/randao/wallet.seed \ No newline at end of file diff --git a/verifiable-delay-function/src/__init__.py b/debian/control similarity index 100% rename from verifiable-delay-function/src/__init__.py rename to debian/control diff --git a/debian/etc/systemd/system/randao.service b/debian/etc/systemd/system/randao.service new file mode 100644 index 0000000..eca398f --- /dev/null +++ b/debian/etc/systemd/system/randao.service @@ -0,0 +1,22 @@ +[Unit] +Description=RANDAO Provider +# When running manually, use the --no-block flag: +# systemctl start --no-block randao.service +Requires=docker.service +After=docker.service +#Wants-randao.timer + +[Service] +Type=simple +User=randao_service +Group=randao_service +WorkingDirectory=/home/randao/RandaoProvider/docker-compose +#ExecStart=/usr/bin/docker compose --env-file /etc/randao/.env up --pull=always +ExecStart=/usr/bin/docker compose -f docker-compose.yml -f docker-compose.appliance.yml --env-file /etc/randao/.env up --pull=always +ExecStop=/usr/bin/docker compose down +TimeoutStartSec=0 +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target diff --git a/debian/etc/systemd/system/randao.timer b/debian/etc/systemd/system/randao.timer new file mode 100644 index 0000000..958852a --- /dev/null +++ b/debian/etc/systemd/system/randao.timer @@ -0,0 +1,22 @@ +Description=Timer to periodically restart RANDAO Provider for latest image pull +#Requires=randao.service +#After=randao.service + +[Timer] +# Restart every day at a random time within the first hour of the day +# OnCalendar=daily +# RandomizedDelaySec=1h +# Persistent=true + +# OR, restart every 12 hours (e.g., 00:00, 12:00) with a random delay +OnCalendar=*-*-* 00,12:00:00 +RandomizedDelaySec=30min +Persistent=true + +# OR, restart every 8 hours from when the service last became active +#OnUnitActiveSec=8h +#RandomizedDelaySec=30min +#AccuracySec=1min # Optional: Reduce timer inaccuracy from default (often 1min) + +[Install] +WantedBy=timers.target diff --git a/debian/etc/update-motd.d/66-randao b/debian/etc/update-motd.d/66-randao new file mode 100644 index 0000000..4a80554 --- /dev/null +++ b/debian/etc/update-motd.d/66-randao @@ -0,0 +1,59 @@ +#!/bin/bash +THIS_SCRIPT="randao" +MOTD_DISABLE="" + +[[ -f /etc/default/armbian-motd ]] && . /etc/default/armbian-motd + +for f in $MOTD_DISABLE; do + [[ $f == $THIS_SCRIPT ]] && exit 0 +done + +## FORMATTING VARIABLES +# --- ANSI Color Codes --- +RED=$'\e[31m' +GREEN=$'\e[32m' +BLUE=$'\e[94m' +YELLOW=$'\e[93m' +GOLD=$'\e[38;5;214m' +ORANGE=$'\e[38;5;208m' +MAGENTA=$'\e[35m' +CYAN=$'\e[36m' + +# if using color with effects, use color first and then the effect. The color codes above reset the effect to none. +BOLD=$'\e[1m' # bold +ITAL=$'\e[3m' # italics +ULINE=$'\e[4m' # underline +XOUT=$'\e[9m' # crossed out +REV=$'\e[7m' # reversed +NC=$'\e[0m' # No Color (resets to default) + + +#servicelist=("randao.service" "randao.timer") +servicelist=("randao.timer") + +for service in "${servicelist[@]}"; do + if [[ -f "/etc/systemd/system/${service}" ]]; then + serviceEnabled=$(systemctl is-enabled $service) + if [[ ${serviceEnabled} == "enabled" ]]; then + serviceEnabled="${BOLD}${GREEN}enabled${NC}" + serviceMessage="" + else + serviceEnabled="${BOLD}${YELLOW}disabled${NC}" + serviceMessage="To enable ${BOLD}${service}${NC}: sudo systemctl enable ${service}" + fi + + serviceActive=$(systemctl is-active $service) + if [[ ${serviceActive} == "active" ]]; then + # service is active + serviceActive="${BOLD}${GREEN}active${NC}" + serviceMessage+="" + else + # service is in-active + serviceActive="${BOLD}${YELLOW}inactive${NC}" + serviceMessage+="\nThen start ${BOLD}${service}${NC}: sudo systemctl start ${service}" + fi + + echo -e "${BOLD}${service} is $serviceEnabled and $serviceActive." + echo -e "${serviceMessage}\n" + fi +done diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..ffaaef1 --- /dev/null +++ b/debian/install @@ -0,0 +1,15 @@ +# Source files from your Git repo (relative to repo root) +# Destination on target system (relative to /) + +# Systemd service and timer files +debian/etc/systemd/system/randao.service /etc/systemd/system/ +debian/etc/systemd/system/randao.timer /etc/systemd/system/ +debian/etc/update-motd.d/66-randao /etc/update-motd.d/66-randao + +# Docker Compose project files +docker-compose/ /opt/randao-provider/ +orchestrator/ /opt/randao-provider/ +puzzle-generator/ /opt/randao-provider/ +requester/ /opt/randao-provider/ +LICENSE /opt/randao-provider/ +README.md /opt/randao-provider/ \ No newline at end of file diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..c97fde1 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,99 @@ +#!/bin/sh +# postinst script for randao-provider Debian package + +set -e # Exit immediately if a command exits with a non-zero status + +# --- 1. Define Paths and Logging --- +LOG_FILE="/var/log/randao-provider-postinst.log" +USERNAME="randao_service" +GROUPNAME="randao_service" +DOCKER_GROUP="docker" +ETC_RANDAO_DIR="/etc/randao" +APP_ROOT_DIR="/opt/randao-provider" + +# Initialize log file with secure permissions +touch "$LOG_FILE" +chmod 600 "$LOG_FILE" + +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - postinst: $1" >> "$LOG_FILE" +} + +log_message "Starting randao-provider post-installation script." + +# --- 2. Create the dedicated system user and group --- +log_message "Checking for user '$USERNAME' and group '$GROUPNAME'." +if ! id -u "$USERNAME" >/dev/null 2>&1; then + log_message "Creating system user '$USERNAME' with a dynamic UID." + # Let the system pick a safe UID automatically + adduser --system --no-create-home --group "$USERNAME" + log_message "User '$USERNAME' created." +else + log_message "User '$USERNAME' already exists. Skipping creation." +fi + +# Add the user to the docker group if it exists +if getent group "$DOCKER_GROUP" >/dev/null 2>&1; then + if ! getent group "$DOCKER_GROUP" | grep -q "\b$USERNAME\b"; then + log_message "Adding user '$USERNAME' to group '$DOCKER_GROUP'." + usermod -aG "$DOCKER_GROUP" "$USERNAME" + log_message "User '$USERNAME' added to '$DOCKER_GROUP'." + else + log_message "User '$USERNAME' is already in group '$DOCKER_GROUP'." + fi +else + log_message "WARNING: Group '$DOCKER_GROUP' does not exist. The service may not function correctly." +fi + +# --- 3. Manage Configuration --- +# NOTE: This section is ideally replaced by using a 'conffiles' file. +# The logic is kept here assuming you are not using conffiles yet. +log_message "Ensuring config directory '$ETC_RANDAO_DIR' exists." +mkdir -p "$ETC_RANDAO_DIR" +chown root:root "$ETC_RANDAO_DIR" +chmod 700 "$ETC_RANDAO_DIR" # Only root can access the directory listing + +TEMPLATE_DIR="$APP_ROOT_DIR/docker-compose/templates" + +# Safely copy example config files if they don't already exist +if [ ! -f "$ETC_RANDAO_DIR/.env" ]; then + log_message "Copying example.env to $ETC_RANDAO_DIR/.env." + cp "$TEMPLATE_DIR/example.env" "$ETC_RANDAO_DIR/.env" +fi +if [ ! -f "$ETC_RANDAO_DIR/wallet.json" ]; then + log_message "Copying example.wallet.json to $ETC_RANDAO_DIR/wallet.json." + cp "$TEMPLATE_DIR/example.wallet.json" "$ETC_RANDAO_DIR/wallet.json" +fi + +# --- 4. Set Secure Permissions for Configuration Files --- +# Set permissions on any config file that exists in the directory. +log_message "Setting ownership and permissions for config files in '$ETC_RANDAO_DIR'." +if [ -f "$ETC_RANDAO_DIR/.env" ]; then + chown root:"$GROUPNAME" "$ETC_RANDAO_DIR/.env" + chmod 640 "$ETC_RANDAO_DIR/.env" # root:rw, group:r, other:--- +fi +if [ -f "$ETC_RANDAO_DIR/wallet.json" ]; then + chown root:"$GROUPNAME" "$ETC_RANDAO_DIR/wallet.json" + chmod 640 "$ETC_RANDAO_DIR/wallet.json" +fi +if [ -f "$ETC_RANDAO_DIR/wallet.seed" ]; then + chown root:"$GROUPNAME" "$ETC_RANDAO_DIR/wallet.seed" + chmod 640 "$ETC_RANDAO_DIR/wallet.seed" +fi + +log_message "Permissions set. User '$USERNAME' (in group '$GROUPNAME') has read-access to configs." +log_message "IMPORTANT: Remember to edit configuration in /etc/randao/ with your actual secrets." + +# --- 5. Enable and Start Systemd Units --- +# NOTE: This section is ideally removed in favor of deb-helper in debian/rules. +# The logic is kept here assuming you are not using deb-helper yet. +log_message "Reloading systemd daemon, then enabling and starting randao.timer." +systemctl daemon-reload +systemctl enable randao.timer +systemctl start --no-block randao.timer +log_message "Systemd randao.timer has been enabled and started." + + +log_message "Randao Provider post-installation script finished." + +exit 0 \ No newline at end of file diff --git a/verifiable-delay-function/src/converters/__init__.py b/debian/prerm similarity index 100% rename from verifiable-delay-function/src/converters/__init__.py rename to debian/prerm diff --git a/debian/rules b/debian/rules new file mode 100644 index 0000000..6a675de --- /dev/null +++ b/debian/rules @@ -0,0 +1,10 @@ +#!/usr/bin/make -f + +%: + dh $@ + +# This block overrides the default 'dh_auto_configure' step. +# It tells deb-helper to run the 'configure' script but with +# an extra option. After this step, the normal sequence continues. +override_dh_auto_configure: + dh_auto_configure -- --with-extra-feature \ No newline at end of file diff --git a/docker-compose/.env.example b/docker-compose/.env.example new file mode 100644 index 0000000..24a371e --- /dev/null +++ b/docker-compose/.env.example @@ -0,0 +1,10 @@ +DB_USER=myuser +DB_PASSWORD=mypassword +DB_NAME=mydatabase +DOCKER_NETWORK=backend +LOG_CONSOLE_LEVEL=3 +## Enable ONE of the wallet methods below: +#SEED_FILE_PATH=/app/config/wallet.seed # path corresponds to the container volume mounted in docker-compose.yml file +#WALLET_JSON_FILE_PATH=/app/config/wallet.json # path corresponds to the container volume mounted in docker-compose.yml file +#SEED_PHRASE="Create a NEW wallet and enter the 12 - 24 words here" +#WALLET_JSON = '{ "kty": "RSA", "e": "test", "n": "test", "d": "test", "p": "test", "q": "test", "dp": "test", "dq": "test", "qi": "test" }' diff --git a/docker-compose/QuickStart.md b/docker-compose/QuickStart.md new file mode 100644 index 0000000..e2b9ead --- /dev/null +++ b/docker-compose/QuickStart.md @@ -0,0 +1,104 @@ +# **Randao Provider: Quick Start Guide** + +This guide provides a quick way to get the Randao Provider running on your local machine using Docker Compose. This is ideal for development, testing, or non-appliance deployments. +**Assumptions:** + +- You have **Docker Desktop** (Windows/macOS) or **Docker Engine** (Linux) installed and running. +- You have basic command-line knowledge. +- You are using the official randao/orchestrator image, which has been pre-built with the necessary wallet management modifications. +- **You have a compatible Arweave wallet (JWK file or mnemonic seed phrase) ready.** + +## **1\. Get the Project Files** + +First, clone the Randao Provider repository from GitHub: +```sh +git clone https://github.com/RandAOLabs/Randomness-Provider.git randao-provider +``` + +Now, navigate into the Docker Compose directory: +``sh +cd randao-provider/docker-compose/ +`` + +## **2\. Prepare Configuration Files** + +You need to create/edit two essential configuration files: .env (for database and logging) and your wallet key file (wallet.json or wallet.seed). + +### **2.1. Create .env (Environment Variables)** + +This file stores your database credentials and other settings. + +1. Copy the example .env file: + cp .env.example .env + +2. Open the newly created .env file in a text editor (e.g., nano .env or code .env) and **fill in your desired values** for the database user, password, and name. You can keep the defaults if running locally for testing. + \# .env + DB\_USER=myuser + DB\_PASSWORD=mypassword + DB\_NAME=mydatabase + DOCKER\_NETWORK=backend + LOG\_CONSOLE\_LEVEL=3 \# Set to 7 for verbose (DEBUG) logs + + +### **2.2. Create Wallet Key File (wallet.json or wallet.seed)** + +This file contains your Arweave wallet's private key (JWK) or mnemonic seed phrase. The application will prioritize reading from wallet.json (JWK) if both are present. If neither file is found, it will fall back to environment variables. We recommend using Wander as the Chrome plugin integrates easily with our [Provider Portal](https://providers_randao.ar.io/providers). + +#### **Option A: Using wallet.json (JWK)** + +1. Copy the example wallet.json file: + cp wallet.json.example wallet.json + +2. Open wallet.json in a text editor and **replace its content with your actual Arweave wallet's JWK (JSON Web Key) data.** + **⚠️ IMPORTANT SECURITY WARNING ⚠️** + + - **NEVER use the example wallet content for a real wallet.** Always generate your own unique Arweave wallet. + - **Keep your wallet.json file secure.** Do not share it or commit it to public repositories. + - On Linux/macOS, it is highly recommended to set strict permissions: + chmod 600 wallet.json + +#### **Option B: Using wallet.seed (Mnemonic Seed Phrase)** + +1. Copy the example wallet.seed file: + cp wallet.seed.example wallet.seed + +2. Open wallet.seed in a text editor and **replace its content with your actual Arweave wallet's mnemonic seed phrase.** The seed phrase must be 12, 18, or 24 words, separated by single spaces, with no extra characters. + **⚠️ IMPORTANT SECURITY WARNING ⚠️** + + - **NEVER use the example seed phrase for a real wallet.** Always generate your own unique Arweave wallet. + - **Keep your wallet.seed file secure.** Do not share it or commit it to public repositories. + - On Linux/macOS, it is highly recommended to set strict permissions: + chmod 600 wallet.seed + +### **2.3. Alternative (Less Secure Fallback): Environment Variables** + +If you prefer not to create wallet.json or wallet.seed files, you can instead add the wallet content directly into your .env file using the WALLET\_JSON or SEED\_PHRASE environment variables. The application will fall back to these if it cannot read from the mounted files. + +- **For JWK:** Add WALLET\_JSON='{"your\_jwk\_content\_here"}' to your .env file. +- **For Seed Phrase:** Add SEED\_PHRASE="your seed phrase words here" to your .env file. + +However, **this method is less secure as environment variables are easily inspectable.** + +## **3\. Run the Randao Provider** + +Now you can start your Docker Compose stack. This command will automatically pull the necessary Docker images and set up your services. +docker compose up \-d \--pull=always + +- \--pull=always: Ensures that Docker always checks for and pulls the latest versions of the images from Docker Hub. +- up: Starts the services in the foreground, showing their logs directly in your terminal. +- \-d: run the container in the background (detached mode) + + +## **4\. Monitor Logs** + +To see the real-time output from your running services (especially for debugging wallet initialization): + +`docker compose logs \-f` + +## **5\. Stop the Randao Provider** + +To stop and remove the running containers, networks, and volumes (excluding named volumes like pgdata): + +`docker compose down` + +You should now have your Randao Provider up and running locally\! \ No newline at end of file diff --git a/docker-compose/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.appliance.yml b/docker-compose/docker-compose.appliance.yml new file mode 100644 index 0000000..10c20a8 --- /dev/null +++ b/docker-compose/docker-compose.appliance.yml @@ -0,0 +1,28 @@ +# /home/randao/Randomness-Provider.git/docker-compose/docker-compose.appliance.yml +services: + orchestrator: + volumes: + # Override wallet mounts to point to /etc/randao/ for appliance security + - /etc/randao/wallet.json:/app/config/wallet.json:ro + - /etc/randao/wallet.seed:/app/config/wallet.seed:ro # If used + deploy: # Appliance-specific orchestrator deploy rules + resources: + limits: + cpus: '1.5' # Allow orchestrator up to 1.5 cores + memory: 300M # Allow up to 300MB RAM + reservations: + cpus: '0.5' # Reserve 0.5 of a core + memory: 192M + + postgres: # Appliance-specific postgres deploy rules and config mount + volumes: + - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro # <--- ADDED HERE + shm_size: '64m' # <--- ADDED HERE (or adjust as needed) + deploy: + resources: + limits: + cpus: '0.4' # Limit Postgres to 40% of one core + memory: 150M # Limit Postgres to 150MB RAM + reservations: + cpus: '0.1' # Reserve 10% of one core + memory: 64M \ No newline at end of file diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml new file mode 100644 index 0000000..b3c35bd --- /dev/null +++ b/docker-compose/docker-compose.yml @@ -0,0 +1,64 @@ +# /home/randao/RandaoProvider/docker-compose/docker-compose.yml (Base) +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 + # REMOVED: - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-mydatabase}"] + interval: 10s + timeout: 5s + retries: 5 + logging: + driver: json-file + options: + max-size: "100m" + max-file: "5" + # REMOVED: shm_size: '64m' - Docker's default shm_size (64MB) will be used + # REMOVED: NO deploy section here; no device-specific limits in the base file. + + orchestrator: + image: hottoddie/orchestrator:custom-file-wallet # Or your chosen stable tag (e.g., appliance-stable) + pull_policy: always + 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} + WALLET_JSON_FILE_PATH: /app/config/wallet.json + SEED_FILE_PATH: /app/config/wallet.seed + WALLET_JSON: ${WALLET_JSON} + DOCKER_NETWORK: backend + networks: + - backend + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./wallet.json:/app/config/wallet.json # Default/Standalone wallet.json mount + - ./wallet.seed:/app/config/wallet.seed # Default/Standalone wallet.seed mount + logging: + driver: json-file + options: + max-size: "100m" + max-file: "5" + +networks: + backend: + name: backend + driver: bridge + +volumes: + pgdata: + driver: local \ No newline at end of file diff --git a/docker-compose/postgres/postgresql.conf b/docker-compose/postgres/postgresql.conf new file mode 100644 index 0000000..c0cb761 --- /dev/null +++ b/docker-compose/postgres/postgresql.conf @@ -0,0 +1,26 @@ +# /home/randao/RandomnProvider/docker-compose/postgres/postgresql.conf +# --- Memory --- +# Total available RAM is 512MB (0.5GB) +# Target for Postgres container is 150MB limit +shared_buffers = 40MB # ~25% of Postgres container's 150MB limit +work_mem = 1MB # For sorting/hashing per operation (start low) +maintenance_work_mem = 16MB # For VACUUM, CREATE INDEX (lower than default) +effective_cache_size = 60MB # Estimate of OS + shared_buffers cache (roughly 40% of container limit) +wal_buffers = 2MB # Smaller WAL buffers for low write loads + +# --- Connections --- +max_connections = 20 # Limit connections to reduce per-connection memory overhead + # Adjust based on orchestrator's needs. + +# --- Checkpointing --- +# Aim to reduce write spikes for low I/O systems +checkpoint_timeout = 10min # Increase from default 5min +max_wal_size = 256MB # Equivalent to ~16 checkpoint segments (16MB each) +checkpoint_completion_target = 0.9 # Spread out checkpoint writes more + +# --- Autovacuum --- +# Autovacuum can be resource-intensive; aggressive settings +# might be needed to keep table bloat down, but tune carefully. +# autovacuum_max_workers = 1 # Reduce workers +# autovacuum_vacuum_scale_factor = 0.05 # Vacuum more frequently on small tables +# autovacuum_analyze_scale_factor = 0.02 # Analyze more frequently diff --git a/docker-compose/wallet.json.example b/docker-compose/wallet.json.example new file mode 100644 index 0000000..72ca0b9 --- /dev/null +++ b/docker-compose/wallet.json.example @@ -0,0 +1,11 @@ +{ + "kty": "RSA", + "e": "AQAB", + "n": "zxP9Y4b1... (truncated for brevity)...eB8zP6Q", + "d": "Jk-9sR1b... (truncated for brevity)...k_1gQ2", + "p": "9h7x0zY0... (truncated for brevity)...wD4q", + "q": "8x6w9yZ8... (truncated for brevity)...vH9k", + "dp": "2o1p3q4r... (truncated for brevity)...sU5t", + "dq": "5t6u7v8w... (truncated for brevity)...xB9y", + "qi": "1a2b3c4d... (truncated for brevity)...gF0h" +} \ No newline at end of file diff --git a/docker-compose/wallet.seed.example b/docker-compose/wallet.seed.example new file mode 100644 index 0000000..f490c8b --- /dev/null +++ b/docker-compose/wallet.seed.example @@ -0,0 +1 @@ +example caution example caution example caution example caution example caution example caution \ No newline at end of file diff --git a/orchestrator/.dockerignore b/orchestrator/.dockerignore new file mode 100644 index 0000000..c4f9ed1 --- /dev/null +++ b/orchestrator/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.git +dist +debian \ No newline at end of file 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..550e187 100644 --- a/orchestrator/docs/development.md +++ b/orchestrator/docs/development.md @@ -0,0 +1,40 @@ +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 + + + +BUGS: +Random deletes itself from the db the moment its been used and not requested. It does not check if it has succeefully used it for challenge AND output first, just checks if its mapped it. This should not be an issue since it waits a day buttttt you know should be fixed with a better check + + + + +# Export version as an environment variable +export VERSION=v1.0.12 # You can change this value to any version you want + +# Build the Docker image with the version tag +docker build -t 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,linux/arm/v7 \ +-t randao/orchestrator:latest \ +-t randao/orchestrator:$VERSION \ +--push . diff --git a/orchestrator/docs/todd-builder.md b/orchestrator/docs/todd-builder.md new file mode 100644 index 0000000..8f7c4ab --- /dev/null +++ b/orchestrator/docs/todd-builder.md @@ -0,0 +1,37 @@ + +# Navigate to your Docker Compose project directory (on your amd64 machine) +cd /path/to/your/local/RandaoProvider/docker-compose/ + +# Export version as an environment variable +export VERSION=v1.0.12 # You can change this value to any version you want + +# Build the Docker image with the version tag +# docker build -t hottoddie/orchestrator:custom-file-wallet -t hottoddie/orchestrator:$VERSION . + +# Log in to Docker +docker login + +# Push the image with the version tag +docker push randao/orchestrator:latest +docker push randao/orchestrator:$VERSION + +# Create and use buildx builder +docker buildx create --use +docker buildx inspect --bootstrap + + + + + +# Build for multiple platforms (including amd64 for testing and arm64 for Orange Pi Zero 3) + + docker buildx build \ + --platform linux/amd64,linux/arm64,linux/arm/v7 \ + -t hottoddie/orchestrator:custom-file-wallet \ + --push \ + -f ../orchestrator/Dockerfile \ + ../orchestrator/ \ No newline at end of file diff --git a/orchestrator/package.json b/orchestrator/package.json new file mode 100644 index 0000000..ccadef7 --- /dev/null +++ b/orchestrator/package.json @@ -0,0 +1,34 @@ +{ + "devDependencies": { + "@types/dockerode": "^3.3.31", + "@types/human-crypto-keys": "^0.1.3", + "@types/node": "^22.9.1", + "@types/pg": "^8.11.10", + "typescript": "^5.6.3" + }, + "dependencies": { + "ao-process-clients": "^6.0.67", + "ao-vrf": "file:", + "arweave": "^1.15.5", + "aws-sdk": "^2.1692.0", + "axios": "^1.7.7", + "bip39": "^3.1.0", + "bip39-web-crypto": "^4.0.1", + "bs58": "^6.0.0", + "check-password-strength": "^3.0.0", + "crypto": "^1.0.1", + "dockerode": "^4.0.2", + "ed25519-hd-key": "^1.3.0", + "human-crypto-keys": "^0.1.4", + "lodash": "^4.17.21", + "pg": "^8.13.1", + "tweetnacl": "^1.0.3", + "typed-assert": "^1.0.9" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/verifiable-delay-function/.env.example b/orchestrator/puzzle-generator/.env.example similarity index 100% rename from verifiable-delay-function/.env.example rename to orchestrator/puzzle-generator/.env.example diff --git a/verifiable-delay-function/.gitignore b/orchestrator/puzzle-generator/.gitignore similarity index 100% rename from verifiable-delay-function/.gitignore rename to orchestrator/puzzle-generator/.gitignore diff --git a/orchestrator/puzzle-generator/.pylintrc b/orchestrator/puzzle-generator/.pylintrc new file mode 100644 index 0000000..4b2ad19 --- /dev/null +++ b/orchestrator/puzzle-generator/.pylintrc @@ -0,0 +1,42 @@ +[MASTER] +# Add the gmpy2 module to the list of known third party modules +extension-pkg-whitelist=gmpy2 + +# Python code to execute, usually for sys.path manipulation such as pygtk.require() +init-hook='import sys; sys.path.append(".")' + +[MESSAGES CONTROL] +# Disable specific warnings +disable=C0111, # Missing docstring + C0103, # Invalid name + C0303, # Trailing whitespace + E1101, # No member (since gmpy2 uses dynamic members) + R0903, # Too Few public methods + +[TYPECHECK] +# List of module names for which member attributes should not be checked +ignored-modules=gmpy2 + +# List of classes names for which member attributes should not be checked +ignored-classes=gmpy2.mpz,gmpy2.random_state + +[FORMAT] +# Maximum number of characters on a single line +max-line-length=100 + +# Number of spaces of indent required inside a hanging or continued line +indent-after-paren=4 + +[BASIC] +# Regular expression which should only match function or class names +function-rgx=[a-z_][a-z0-9_]{2,50}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +[REPORTS] +# Set the output format. Available formats are text, parseable, colorized +output-format=colorized + +# Include a brief explanation of each error when errors are displayed +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} diff --git a/orchestrator/puzzle-generator/Dockerfile b/orchestrator/puzzle-generator/Dockerfile new file mode 100644 index 0000000..be0ffdf --- /dev/null +++ b/orchestrator/puzzle-generator/Dockerfile @@ -0,0 +1,35 @@ +# Use an official Python image as a base +FROM python:3.12 + +# Install system dependencies needed for gmpy2 and PostgreSQL connection +RUN apt-get update && apt-get install -y \ + libgmp-dev \ + libmpfr-dev \ + libmpc-dev \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Set up a working directory +WORKDIR /app + +# Copy only the requirements file to leverage Docker cache +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code +COPY . . + +# Set environment variables for PostgreSQL credentials (can be overridden at runtime) +ENV DB_NAME=mydatabase \ + DB_USER=myuser \ + DB_PASSWORD=mypassword \ + DB_HOST=localhost \ + DB_PORT=5432 + +# Expose any necessary ports (optional, specify if your app uses specific ports) +# EXPOSE 8000 + +# Command to run the main script +# CMD ["python", "main.py"] diff --git a/orchestrator/puzzle-generator/README.md b/orchestrator/puzzle-generator/README.md new file mode 100644 index 0000000..687e29f --- /dev/null +++ b/orchestrator/puzzle-generator/README.md @@ -0,0 +1,27 @@ +# [🔙](../) Time-Lock Puzzles +This repository section contains an implementation of [Time-Lock Puzzles](https://en.wikipedia.org/wiki/Time-lock_puzzle) as outlined in the seminal paper [Time-lock puzzles and timed-release Crypto](https://people.csail.mit.edu/rivest/pubs/RSW96.pdf) by Ronald L. Rivest, Adi Shamir, and David A. Wagner. + +This Time-Lock Puzzle implementation is part of **RandAO's Randomness Provider** project, designed to provide a reliable source of randomness based on cryptographic time delays. RandAO's Randomness Provider leverages Time-Lock Puzzles to ensure that randomness generation requires a precise amount of sequential computation time, establishing trust and security for applications requiring provably delayed randomness. + +## Table of Contents +- [Overview](#overview) +- [Development](#development) +- [License](#license) + +## Overview +The Time-Lock Puzzle implementation in this repository follows the specifications in the [RSW96 paper](https://people.csail.mit.edu/rivest/pubs/RSW96.pdf), providing a cryptographically secure mechanism for creating puzzles that require a predetermined amount of sequential computation to solve. This feature is crucial for applications in time-released cryptography and decentralized randomness protocols, where it is essential to produce randomness that cannot be accessed before a specific time has elapsed. + +Key features of this Time-Lock Puzzle implementation include: + + - Sequential Computation: The puzzle's design requires a specific number of sequential squaring operations modulo a composite number, ensuring that parallel computing offers no advantage in solving the puzzle. + - Precise Time Calibration: The difficulty of each puzzle can be precisely calibrated based on the computing power available to the solver. + - Efficient Creation: Puzzles can be created efficiently by anyone who knows the factorization of the modulus. + - Secure Message Encryption: The puzzle can securely encrypt a message that remains hidden until the sequential computation is completed. + +This approach enables decentralized protocols to produce randomness that is guaranteed to remain secret for a specific time period, making it ideal for use cases such as secure time-released cryptography, fair contract signing, sealed-bid auctions, and other applications requiring temporal security guarantees. + +## Development +For detailed development guidelines, including contributing, testing, and documentation, please refer to the [Development Documentation](./docs/developing.md). + +## License +This project is licensed under the MIT License. See the [LICENSE file](../LICENSE) for details. diff --git a/verifiable-delay-function/conftest.py b/orchestrator/puzzle-generator/conftest.py similarity index 100% rename from verifiable-delay-function/conftest.py rename to orchestrator/puzzle-generator/conftest.py diff --git a/verifiable-delay-function/docs/developing.md b/orchestrator/puzzle-generator/docs/developing.md similarity index 53% rename from verifiable-delay-function/docs/developing.md rename to orchestrator/puzzle-generator/docs/developing.md index 6322f61..6a6291b 100644 --- a/verifiable-delay-function/docs/developing.md +++ b/orchestrator/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/orchestrator/puzzle-generator/main.py b/orchestrator/puzzle-generator/main.py new file mode 100644 index 0000000..a7d10d7 --- /dev/null +++ b/orchestrator/puzzle-generator/main.py @@ -0,0 +1,124 @@ +"""Main script for generating and persisting time lock puzzles.""" + +import argparse +import time +from typing import List, Tuple + +from src.converters.rsa_converter import RSAConverter +from src.converters.time_lock_puzzle_converter import TimeLockPuzzleConverter +from src.database.DatabaseService import DatabaseService +from src.database.entity.RSAEntity import RSAEntity +from src.database.entity.TimeLockPuzzleEntity import TimeLockPuzzleEntity +from src.mpc import MPC +from src.mpc.types import MPZ +from src.protocol_constants import BIT_SIZE, TIMING_PARAMETER +from src.rsa.RSA import RSA +from src.time_lock_puzzle.TimeLockPuzzle import TimeLockPuzzle +from src.time_lock_puzzle.TimeLockPuzzleFactory import TimeLockPuzzleFactory + + +class TimeLockPuzzleService: + """Service class for managing time lock puzzle operations.""" + + def __init__(self, bit_size: int, timing_parameter: MPC.mpz): + """ + Initialize the service. + + Args: + bit_size: Size for RSA parameters + timing_parameter: Number of squarings required + """ + self.factory = TimeLockPuzzleFactory(bit_size, timing_parameter) + self.rsa_converter = RSAConverter() + self.puzzle_converter = TimeLockPuzzleConverter() + + def generate_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + """ + Generate multiple time lock puzzles. + + Args: + amount: Number of puzzles to generate + + Returns: + List of (puzzle, rsa) tuples + """ + print(f"Generating {amount} puzzles...") + start_time = time.time() + puzzles = self.factory.create_puzzles(amount) + + total_time = time.time() - start_time + print(f"Puzzle generation took {total_time:.2f} seconds") + return puzzles + + def convert_to_entities( + self, puzzles: List[Tuple[TimeLockPuzzle, RSA, MPZ]] + ) -> List[TimeLockPuzzleEntity | RSAEntity]: + """ + Convert puzzles and RSAs to database entities. + + Args: + puzzles: List of (puzzle, rsa) tuples + + Returns: + List of entities to save + """ + print("\nConverting to entities...") + start_time = time.time() + entities = [] + for puzzle, rsa, y in puzzles: + # Convert RSA entity first to get its ID (now generated on creation) + rsa_entity = self.rsa_converter.to_entity(rsa) + # Create puzzle entity with the generated RSA ID + puzzle_entity = self.puzzle_converter.to_entity(puzzle, rsa_entity.id, y) + entities.extend([rsa_entity, puzzle_entity]) + total_time = time.time() - start_time + print(f"Entity conversion took {total_time:.2f} seconds") + return entities + + def save_entities(self, entities: List[TimeLockPuzzleEntity | RSAEntity]) -> None: + """ + Save entities to database. + + Args: + entities: List of entities to save + """ + print("\nSaving to database...") + start_time = time.time() + # Now we can save all entities at once since RSA IDs are generated on creation + DatabaseService.save_many(entities) + total_time = time.time() - start_time + print(f"Database save took {total_time:.2f} seconds") + + +def parse_args() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Generate and save time lock puzzles.") + parser.add_argument( + "count", + type=int, + help="Number of time lock puzzles to generate", + ) + return parser.parse_args() + + +def main() -> None: + """Generate time lock puzzles and save them to the database.""" + args = parse_args() + + # Initialize service + service = TimeLockPuzzleService(BIT_SIZE, TIMING_PARAMETER) + + # Generate puzzles + puzzles = service.generate_puzzles(args.count) + + # Convert to entities + entities = service.convert_to_entities(puzzles) + + # Save to database + service.save_entities(entities) + + print("\nDone!") + + +if __name__ == "__main__": + main() diff --git a/verifiable-delay-function/requirements.txt b/orchestrator/puzzle-generator/requirements.txt similarity index 100% rename from verifiable-delay-function/requirements.txt rename to orchestrator/puzzle-generator/requirements.txt diff --git a/verifiable-delay-function/src/database/__init__.py b/orchestrator/puzzle-generator/src/__init__.py similarity index 100% rename from verifiable-delay-function/src/database/__init__.py rename to orchestrator/puzzle-generator/src/__init__.py diff --git a/orchestrator/puzzle-generator/src/converters/__init__.py b/orchestrator/puzzle-generator/src/converters/__init__.py new file mode 100644 index 0000000..ee1ed54 --- /dev/null +++ b/orchestrator/puzzle-generator/src/converters/__init__.py @@ -0,0 +1,6 @@ +"""Converters for database entities.""" + +from .time_lock_puzzle_converter import TimeLockPuzzleConverter +from .rsa_converter import RSAConverter + +__all__ = ["TimeLockPuzzleConverter", "RSAConverter"] diff --git a/orchestrator/puzzle-generator/src/converters/rsa_converter.py b/orchestrator/puzzle-generator/src/converters/rsa_converter.py new file mode 100644 index 0000000..209308c --- /dev/null +++ b/orchestrator/puzzle-generator/src/converters/rsa_converter.py @@ -0,0 +1,25 @@ +"""Converter for RSA objects.""" + +from src.rsa.RSA import RSA +from src.database.entity.RSAEntity import RSAEntity + + +class RSAConverter: + """Converter for storing RSA parameters in the database.""" + + @staticmethod + def to_entity(rsa: RSA) -> RSAEntity: + """Convert an RSA instance to an RSAEntity. + + Args: + rsa (RSA): The RSA instance to convert + + Returns: + RSAEntity: The database entity + """ + return RSAEntity( + hex(rsa.get_p())[2:], # remove 0x + hex(rsa.get_q())[2:], # remove 0x + hex(rsa.get_N())[2:], # remove 0x + hex(rsa.get_phi())[2:], # remove 0x + ) diff --git a/orchestrator/puzzle-generator/src/converters/time_lock_puzzle_converter.py b/orchestrator/puzzle-generator/src/converters/time_lock_puzzle_converter.py new file mode 100644 index 0000000..4eb498d --- /dev/null +++ b/orchestrator/puzzle-generator/src/converters/time_lock_puzzle_converter.py @@ -0,0 +1,29 @@ +"""Converter for time lock puzzle objects.""" + +from src.time_lock_puzzle.TimeLockPuzzle import TimeLockPuzzle +from src.database.entity.TimeLockPuzzleEntity import TimeLockPuzzleEntity +from src.mpc.types import MPZ + + +class TimeLockPuzzleConverter: + """Converter between TimeLockPuzzle and TimeLockPuzzleEntity.""" + + @staticmethod + def to_entity(puzzle: TimeLockPuzzle, rsa_id: str, y: MPZ) -> TimeLockPuzzleEntity: + """Convert a TimeLockPuzzle to a TimeLockPuzzleEntity. + + Args: + puzzle (TimeLockPuzzle): The puzzle to convert + rsa_id (str): ID of the associated RSA entity + y (MPZ): The y value from the puzzle tuple + + Returns: + TimeLockPuzzleEntity: The database entity + """ + return TimeLockPuzzleEntity( + x_hex=hex(puzzle.get_x())[2:], # remove 0x + y_hex=hex(y)[2:], # remove 0x + t=str(puzzle.get_t()), # remove 0x + N_hex=hex(puzzle.get_N())[2:], # remove 0x + rsa_id=rsa_id, + ) diff --git a/orchestrator/puzzle-generator/src/database/DatabaseService.py b/orchestrator/puzzle-generator/src/database/DatabaseService.py new file mode 100644 index 0000000..0df181e --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/DatabaseService.py @@ -0,0 +1,17 @@ +from typing import List +from .mixins.saveable import Saveable + + +class DatabaseService: + """Service class for database operations.""" + + @staticmethod + def save_many(instances: List[Saveable]) -> None: + """ + Save multiple instances to the database. + + Args: + instances: List of Saveable instances to save + """ + for instance in instances: + instance.save() diff --git a/verifiable-delay-function/src/database/entity/__init__.py b/orchestrator/puzzle-generator/src/database/__init__.py similarity index 100% rename from verifiable-delay-function/src/database/entity/__init__.py rename to orchestrator/puzzle-generator/src/database/__init__.py diff --git a/verifiable-delay-function/src/database/constants.py b/orchestrator/puzzle-generator/src/database/constants.py similarity index 100% rename from verifiable-delay-function/src/database/constants.py rename to orchestrator/puzzle-generator/src/database/constants.py diff --git a/verifiable-delay-function/src/database/database.py b/orchestrator/puzzle-generator/src/database/database.py similarity index 100% rename from verifiable-delay-function/src/database/database.py rename to orchestrator/puzzle-generator/src/database/database.py diff --git a/orchestrator/puzzle-generator/src/database/entity/RSAEntity.py b/orchestrator/puzzle-generator/src/database/entity/RSAEntity.py new file mode 100644 index 0000000..7ddb16f --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/entity/RSAEntity.py @@ -0,0 +1,43 @@ +import uuid +from sqlalchemy import Column, String +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship + +from src.database.mixins.saveable import Saveable +from src.database.database import get_orm_base + +# Define the Base class for ORM models +Base = get_orm_base() + + +class RSAEntity(Base, Saveable): + """Database entity for storing RSA parameters.""" + + __tablename__ = "rsa_keys" + + id = Column(String, primary_key=True) # Unique generated string ID + p = Column(String, nullable=False) # Store hex string of prime p + q = Column(String, nullable=False) # Store hex string of prime q + modulus = Column(String, nullable=False) # Store hex string of modulus N + phi = Column(String, nullable=False) # Store hex string of Euler's totient + puzzle = relationship( + "TimeLockPuzzleEntity", back_populates="rsa", uselist=False + ) # One-to-one back reference to puzzle + + def __repr__(self): + return f"" + + def __init__(self, p_hex: str, q_hex: str, N_hex: str, phi_hex: str): + """Initialize an RSA entity. + + Args: + p_hex (str): Hex string of prime p + q_hex (str): Hex string of prime q + N_hex (str): Hex string of modulus N + phi_hex (str): Hex string of Euler's totient + """ + self.id = str(uuid.uuid4()) # Generate ID on creation + self.p = p_hex + self.q = q_hex + self.modulus = N_hex + self.phi = phi_hex diff --git a/orchestrator/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py b/orchestrator/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py new file mode 100644 index 0000000..487f5db --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/entity/TimeLockPuzzleEntity.py @@ -0,0 +1,49 @@ +import uuid +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.orm import relationship + +from src.database.mixins.saveable import Saveable +from src.database.database import get_orm_base + +# Define the Base class for ORM models +Base = get_orm_base() + + +class TimeLockPuzzleEntity(Base, Saveable): + """Database entity for storing time lock puzzles.""" + + __tablename__ = "time_lock_puzzles" + + id = Column( + String, primary_key=True, default=lambda: str(uuid.uuid4()) + ) # Unique generated string ID + x = Column(String, nullable=False) # Store hex string of input value x + y = Column(String, nullable=False) # Store hex string of y value + t = Column(String, nullable=False) # Store base 10 string of time parameter t + modulus = Column(String, nullable=False) # Store hex string of modulus N + request_id = Column( + String, nullable=True + ) # Optional associated randomness request id (will be filled within the provider node runtime) + rsa_id = Column( + String, ForeignKey("rsa_keys.id"), nullable=False, unique=True + ) # One-to-one reference to RSA key + rsa = relationship( + "RSAEntity", back_populates="puzzle" + ) # One-to-one relationship to RSA entity + + def __repr__(self): + return f"" + + def __init__(self, x_hex: str, y_hex: str, t: str, N_hex: str, rsa_id: str): + """Initialize a time lock puzzle entity. + + Args: + x_hex (str): Hex string of input value x + t (str): Base 10 string of time parameter t + N_hex (str): Hex string of modulus N + """ + self.x = x_hex + self.y = y_hex + self.t = t + self.modulus = N_hex + self.rsa_id = rsa_id diff --git a/orchestrator/puzzle-generator/src/database/entity/__init__.py b/orchestrator/puzzle-generator/src/database/entity/__init__.py new file mode 100644 index 0000000..4f7ba70 --- /dev/null +++ b/orchestrator/puzzle-generator/src/database/entity/__init__.py @@ -0,0 +1,6 @@ +"""Database entity models.""" + +from .TimeLockPuzzleEntity import TimeLockPuzzleEntity +from .RSAEntity import RSAEntity + +__all__ = ["TimeLockPuzzleEntity", "RSAEntity"] diff --git a/verifiable-delay-function/src/database/initialize_db.py b/orchestrator/puzzle-generator/src/database/initialize_db.py similarity index 86% rename from verifiable-delay-function/src/database/initialize_db.py rename to orchestrator/puzzle-generator/src/database/initialize_db.py index b25acb3..fb236c4 100644 --- a/verifiable-delay-function/src/database/initialize_db.py +++ b/orchestrator/puzzle-generator/src/database/initialize_db.py @@ -5,10 +5,11 @@ from sqlalchemy.exc import OperationalError # Dynamically add the `src` directory to `sys.path` -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) +from src.database.entity import * from src.database.database import get_engine, Base -from src.database.entity.verifiable_delay_function_entity import VerifiableDelayFunctionEntity + def initialize_database(): """ @@ -24,6 +25,7 @@ def initialize_database(): finally: engine.dispose() # Close the engine when done + if __name__ == "__main__": - + initialize_database() diff --git a/verifiable-delay-function/src/database/mixins/__init__.py b/orchestrator/puzzle-generator/src/database/mixins/__init__.py similarity index 100% rename from verifiable-delay-function/src/database/mixins/__init__.py rename to orchestrator/puzzle-generator/src/database/mixins/__init__.py diff --git a/verifiable-delay-function/src/database/mixins/saveable.py b/orchestrator/puzzle-generator/src/database/mixins/saveable.py similarity index 100% rename from verifiable-delay-function/src/database/mixins/saveable.py rename to orchestrator/puzzle-generator/src/database/mixins/saveable.py diff --git a/orchestrator/puzzle-generator/src/mpc/MPC.py b/orchestrator/puzzle-generator/src/mpc/MPC.py new file mode 100644 index 0000000..169c273 --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/MPC.py @@ -0,0 +1,35 @@ +import gmpy2 +from .abstract.IMPC import IMPC +from .types import MPZ, RandomState + + +class MPC(IMPC): + """Implementation of multi-precision computing operations.""" + + @staticmethod + def mpz(value: int) -> MPZ: + return gmpy2.mpz(value) + + @staticmethod + def random_state(seed: int) -> RandomState: + return gmpy2.random_state(seed) + + @staticmethod + def mpz_urandomb(state: RandomState, bit_count: int) -> MPZ: + return gmpy2.mpz_urandomb(state, bit_count) + + @staticmethod + def next_prime(value: MPZ) -> MPZ: + return gmpy2.next_prime(value) + + @staticmethod + def powmod(base: MPZ, exp: MPZ, mod: MPZ) -> MPZ: + return gmpy2.powmod(base, exp, mod) + + @staticmethod + def pow(base: MPZ, exp: MPZ) -> MPZ: + return base**exp + + @staticmethod + def mod(value: MPZ, modulus: MPZ) -> MPZ: + return value % modulus # gmpy2 supports % operator for mpz values diff --git a/orchestrator/puzzle-generator/src/mpc/__init__.py b/orchestrator/puzzle-generator/src/mpc/__init__.py new file mode 100644 index 0000000..ab216aa --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/__init__.py @@ -0,0 +1,7 @@ +"""Multi-precision computing module.""" + +from .MPC import MPC +from .abstract.IMPC import IMPC +from .types import MPZ, RandomState, T + +__all__ = ["MPC", "IMPC", "MPZ", "RandomState", "T"] diff --git a/orchestrator/puzzle-generator/src/mpc/abstract/IMPC.py b/orchestrator/puzzle-generator/src/mpc/abstract/IMPC.py new file mode 100644 index 0000000..4912662 --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/abstract/IMPC.py @@ -0,0 +1,95 @@ +from abc import ABC, abstractmethod +from ..types import MPZ, RandomState + + +class IMPC(ABC): + """Abstract base class defining the interface for multi-precision computing operations.""" + + @staticmethod + @abstractmethod + def mpz(value: int) -> MPZ: + """Convert a Python integer to an mpz. + + Args: + value (int): Integer value to convert + + Returns: + mpz: Multi-precision integer + """ + + @staticmethod + @abstractmethod + def random_state(seed: int) -> RandomState: + """Create a random state from a seed. + + Args: + seed (int): Seed value for random state + + Returns: + mpz: Random state object + """ + + @staticmethod + @abstractmethod + def mpz_urandomb(state: RandomState, bit_count: int) -> MPZ: + """Generate a random integer with specified number of bits. + + Args: + state (mpz): Random state to use + bit_count (int): Number of bits in result + + Returns: + mpz: Random integer + """ + + @staticmethod + @abstractmethod + def next_prime(value: MPZ) -> MPZ: + """Find the next prime number after the given value. + + Args: + value (mpz): Starting value + + Returns: + mpz: Next prime number + """ + + @staticmethod + @abstractmethod + def powmod(base: MPZ, exp: MPZ, mod: MPZ) -> MPZ: + """Compute (base ** exp) % mod efficiently. + + Args: + base (mpz): Base value + exp (mpz): Exponent value + mod (mpz): Modulus value + + Returns: + mpz: Result of modular exponentiation + """ + + @staticmethod + @abstractmethod + def pow(base: MPZ, exp: MPZ) -> MPZ: + """Compute base ** exp. + + Args: + base (mpz): Base value + exp (mpz): Exponent value + + Returns: + mpz: Result of exponentiation + """ + + @staticmethod + @abstractmethod + def mod(value: MPZ, modulus: MPZ) -> MPZ: + """Compute value % modulus. + + Args: + value (mpz): Value to reduce + modulus (mpz): Modulus to reduce by + + Returns: + mpz: Result of modular reduction + """ diff --git a/orchestrator/puzzle-generator/src/mpc/abstract/__init__.py b/orchestrator/puzzle-generator/src/mpc/abstract/__init__.py new file mode 100644 index 0000000..557827b --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for multi-precision computing operations.""" + +from .IMPC import IMPC + +__all__ = ["IMPC"] diff --git a/orchestrator/puzzle-generator/src/mpc/types.py b/orchestrator/puzzle-generator/src/mpc/types.py new file mode 100644 index 0000000..b9497fd --- /dev/null +++ b/orchestrator/puzzle-generator/src/mpc/types.py @@ -0,0 +1,11 @@ +"""Type definitions for multi-precision computing operations.""" + +from typing import TypeVar, NewType +from gmpy2 import mpz as _mpz, random_state as _random_state + +# Define base types from gmpy2 +MPZ = NewType("MPZ", _mpz) +RandomState = NewType("RandomState", _random_state) + +# Generic type variable for numeric operations +T = TypeVar("T", MPZ, int) diff --git a/orchestrator/puzzle-generator/src/primes/Primes.py b/orchestrator/puzzle-generator/src/primes/Primes.py new file mode 100644 index 0000000..6615545 --- /dev/null +++ b/orchestrator/puzzle-generator/src/primes/Primes.py @@ -0,0 +1,18 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from ..random import Random +from .abstract.IPrimes import IPrimes + + +class Primes(IPrimes): + """Implementation of prime number generation.""" + + @staticmethod + def get_prime(bit_size: int) -> MPZ: + # Get random state for generating random numbers + rand = Random.get_random(bit_size) + + random_num = MPC.mpz_urandomb(rand, bit_size) + + # Get next prime after the random number + return MPC.next_prime(random_num) diff --git a/orchestrator/puzzle-generator/src/primes/__init__.py b/orchestrator/puzzle-generator/src/primes/__init__.py new file mode 100644 index 0000000..52cdf79 --- /dev/null +++ b/orchestrator/puzzle-generator/src/primes/__init__.py @@ -0,0 +1,6 @@ +"""Prime number generation module.""" + +from .Primes import Primes +from .abstract.IPrimes import IPrimes + +__all__ = ["Primes", "IPrimes"] diff --git a/orchestrator/puzzle-generator/src/primes/abstract/IPrimes.py b/orchestrator/puzzle-generator/src/primes/abstract/IPrimes.py new file mode 100644 index 0000000..2d49682 --- /dev/null +++ b/orchestrator/puzzle-generator/src/primes/abstract/IPrimes.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class IPrimes(ABC): + """Abstract base class defining the interface for prime number generation.""" + + @staticmethod + @abstractmethod + def get_prime(bit_size: int) -> MPZ: + """Get a random prime number. + + Args: + bit_size (int): Number of bits for the prime number. + + Returns: + MPZ: A random prime number + """ diff --git a/orchestrator/puzzle-generator/src/primes/abstract/__init__.py b/orchestrator/puzzle-generator/src/primes/abstract/__init__.py new file mode 100644 index 0000000..0ee899c --- /dev/null +++ b/orchestrator/puzzle-generator/src/primes/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for prime number generation.""" + +from .IPrimes import IPrimes + +__all__ = ["IPrimes"] diff --git a/orchestrator/puzzle-generator/src/protocol_constants.py b/orchestrator/puzzle-generator/src/protocol_constants.py new file mode 100644 index 0000000..15e0067 --- /dev/null +++ b/orchestrator/puzzle-generator/src/protocol_constants.py @@ -0,0 +1,7 @@ +# protocol_constants.py + +from src.mpc import MPC + + +BIT_SIZE = 2048 # RSA modulus bit size +TIMING_PARAMETER = MPC.mpz(3_000_000) # T - Total squarings for delay diff --git a/orchestrator/puzzle-generator/src/random/Random.py b/orchestrator/puzzle-generator/src/random/Random.py new file mode 100644 index 0000000..d5e1458 --- /dev/null +++ b/orchestrator/puzzle-generator/src/random/Random.py @@ -0,0 +1,13 @@ +import secrets +from ..mpc import MPC +from ..mpc.types import RandomState +from .abstract.IRandom import IRandom + + +class Random(IRandom): + """Implementation of secure random number generation.""" + + @staticmethod + def get_random(bit_size: int) -> RandomState: + secure_seed = secrets.randbits(bit_size) + return MPC.random_state(secure_seed) diff --git a/orchestrator/puzzle-generator/src/random/__init__.py b/orchestrator/puzzle-generator/src/random/__init__.py new file mode 100644 index 0000000..3c8b236 --- /dev/null +++ b/orchestrator/puzzle-generator/src/random/__init__.py @@ -0,0 +1,6 @@ +"""Random number generation module.""" + +from .Random import Random +from .abstract.IRandom import IRandom + +__all__ = ["Random", "IRandom"] diff --git a/orchestrator/puzzle-generator/src/random/abstract/IRandom.py b/orchestrator/puzzle-generator/src/random/abstract/IRandom.py new file mode 100644 index 0000000..2e6a0ce --- /dev/null +++ b/orchestrator/puzzle-generator/src/random/abstract/IRandom.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from ...mpc.types import RandomState + + +class IRandom(ABC): + """Abstract base class defining the interface for random number generation.""" + + @staticmethod + @abstractmethod + def get_random(bit_size: int) -> RandomState: + """Get a random state initialized with a secure seed. + + Args: + bit_size (int): Number of bits for the secure seed. + + Returns: + RandomState: A random state initialized with a secure seed + """ diff --git a/orchestrator/puzzle-generator/src/random/abstract/__init__.py b/orchestrator/puzzle-generator/src/random/abstract/__init__.py new file mode 100644 index 0000000..b22a9fd --- /dev/null +++ b/orchestrator/puzzle-generator/src/random/abstract/__init__.py @@ -0,0 +1,5 @@ +"""Abstract interfaces for random number generation.""" + +from .IRandom import IRandom + +__all__ = ["IRandom"] diff --git a/orchestrator/puzzle-generator/src/rsa/RSA.py b/orchestrator/puzzle-generator/src/rsa/RSA.py new file mode 100644 index 0000000..45b5bae --- /dev/null +++ b/orchestrator/puzzle-generator/src/rsa/RSA.py @@ -0,0 +1,52 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from .abstract.IRSA import IRSA +from ..primes import Primes + + +class RSA(IRSA): + """Implementation of RSA cryptosystem.""" + + def __init__(self, bit_size: int) -> None: + """Initialize RSA by generating two random prime numbers. + + Args: + bit_size (int): Number of bits for RSA modulus. + Each prime will be bit_size/2 bits. + """ + # Generate two random prime numbers + prime_size = ( + bit_size // 2 - 1 + ) # Each prime is half the size TODO is this needed anymore with gmpc on chain? + self._p = Primes.get_prime(prime_size) + self._q = Primes.get_prime(prime_size) + + # Calculate modulus N and Euler's totient + self._N = self._calculate_N() + self._phi = self._calculate_phi() + + def get_p(self) -> MPZ: + return self._p + + def get_q(self) -> MPZ: + return self._q + + def get_N(self) -> MPZ: + return self._N + + def get_phi(self) -> MPZ: + return self._phi + + def get_eulers_totient(self) -> MPZ: + return self.get_phi() + + # Private methods + # -------------- + + def _calculate_N(self) -> MPZ: + """Calculate the RSA modulus N = p * q.""" + return MPC.mpz(self._p * self._q) + + def _calculate_phi(self) -> MPZ: + """Calculate Euler's totient φ(N) = (p-1)(q-1).""" + return MPC.mpz((self._p - 1) * (self._q - 1)) diff --git a/orchestrator/puzzle-generator/src/rsa/__init__.py b/orchestrator/puzzle-generator/src/rsa/__init__.py new file mode 100644 index 0000000..4ac513e --- /dev/null +++ b/orchestrator/puzzle-generator/src/rsa/__init__.py @@ -0,0 +1,6 @@ +"""RSA cryptosystem module.""" + +from .RSA import RSA +from .abstract.IRSA import IRSA + +__all__ = ["RSA", "IRSA"] diff --git a/orchestrator/puzzle-generator/src/rsa/abstract/IRSA.py b/orchestrator/puzzle-generator/src/rsa/abstract/IRSA.py new file mode 100644 index 0000000..9897017 --- /dev/null +++ b/orchestrator/puzzle-generator/src/rsa/abstract/IRSA.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class IRSA(ABC): + """Abstract base class defining the interface for RSA cryptosystem implementation.""" + + @abstractmethod + def get_p(self) -> MPZ: + """Get the first prime factor p. + + Returns: + MPZ: The prime number p + """ + + @abstractmethod + def get_q(self) -> MPZ: + """Get the second prime factor q. + + Returns: + MPZ: The prime number q + """ + + @abstractmethod + def get_N(self) -> MPZ: + """Get the modulus N = p * q. + + Returns: + MPZ: The modulus N + """ + + @abstractmethod + def get_phi(self) -> MPZ: + """Get Euler's totient φ(N) = (p-1)(q-1). + + Returns: + MPZ: The value of Euler's totient function + """ + + @abstractmethod + def get_eulers_totient(self) -> MPZ: + """Alias for get_phi(). + + Returns: + MPZ: The value of Euler's totient function + """ diff --git a/verifiable-delay-function/src/verifiable_delay_function/__init__.py b/orchestrator/puzzle-generator/src/rsa/abstract/__init__.py similarity index 100% rename from verifiable-delay-function/src/verifiable_delay_function/__init__.py rename to orchestrator/puzzle-generator/src/rsa/abstract/__init__.py diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py new file mode 100644 index 0000000..996909f --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py @@ -0,0 +1,45 @@ +from multiprocessing import Pool +from typing import List, Tuple + +from ..mpc import MPC +from ..mpc.types import MPZ +from ..rsa.RSA import RSA +from .abstract.IEfficientTimeLockPuzzleSolver import IEfficientTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .constants import TWO + + +class EfficientTimeLockPuzzleSolver(IEfficientTimeLockPuzzleSolver): + """Implementation of efficient time lock puzzle solver using RSA private parameters.""" + + @staticmethod + def solve(rsa: RSA, puzzle: ITimeLockPuzzle) -> MPZ: + # Calculate y = x^(2^t) mod N efficiently using phi + # Calculate 2^t + exp = MPC.pow(TWO, puzzle.get_t()) # 2^t + phi = rsa.get_phi() + d = MPC.mod(exp, phi) # Reduce exponent modulo phi + return MPC.powmod(puzzle.get_x(), d, puzzle.get_N()) + + @staticmethod + def solve_many(puzzles: List[Tuple[RSA, ITimeLockPuzzle]]) -> List[MPZ]: + """ + Solve multiple time lock puzzles in parallel using multiprocessing. + + Args: + puzzles: List of tuples containing (RSA, puzzle) pairs to solve + + Returns: + List of solutions in the same order as input puzzles + """ + with Pool() as pool: + return pool.map(EfficientTimeLockPuzzleSolver._solve_single, puzzles) + + # Private Methods + # -------------- + + @staticmethod + def _solve_single(args: Tuple[RSA, ITimeLockPuzzle]) -> MPZ: + """Helper method to solve a single puzzle for multiprocessing.""" + rsa, puzzle = args + return EfficientTimeLockPuzzleSolver.solve(rsa, puzzle) diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py new file mode 100644 index 0000000..0144200 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/SequentialTimeLockPuzzleSolver.py @@ -0,0 +1,33 @@ +from ..mpc import MPC +from ..mpc.types import MPZ +from .abstract.ISequentialTimeLockPuzzleSolver import ISequentialTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .constants import TWO + + +class SequentialTimeLockPuzzleSolver(ISequentialTimeLockPuzzleSolver): + """Implementation of sequential time lock puzzle solver.""" + + @staticmethod + def solve(puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle sequentially without RSA private parameters. + + This implementation: + 1. Calculates 2^t directly + 2. Then computes x^(2^t) mod N in one step using powmod + + Args: + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ + x = puzzle.get_x() + N = puzzle.get_N() + t = puzzle.get_t() + + # Calculate 2^t first + exp = MPC.pow(TWO, t) + + # Then calculate x^(2^t) mod N in one step + return MPC.powmod(x, exp, N) diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py new file mode 100644 index 0000000..65a0a51 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzle.py @@ -0,0 +1,27 @@ +from ..mpc.types import MPZ +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle + + +class TimeLockPuzzle(ITimeLockPuzzle): + """Implementation of a time lock puzzle.""" + + def __init__(self, x: MPZ, t: MPZ, N: MPZ) -> None: + """Initialize a time lock puzzle. + + Args: + x (MPZ): The input value + t (MPZ): The time parameter + N (MPZ): The modulus + """ + self._x = x + self._t = t + self._N = N + + def get_x(self) -> MPZ: + return self._x + + def get_t(self) -> MPZ: + return self._t + + def get_N(self) -> MPZ: + return self._N diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py new file mode 100644 index 0000000..be6e9da --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleBuilder.py @@ -0,0 +1,30 @@ +from typing import Self +from ..mpc.types import MPZ +from .TimeLockPuzzle import TimeLockPuzzle +from .abstract.ITimeLockPuzzleBuilder import ITimeLockPuzzleBuilder + + +class TimeLockPuzzleBuilder(ITimeLockPuzzleBuilder): + """Implementation of time lock puzzle builder.""" + + def __init__(self) -> None: + self._x = None + self._t = None + self._N = None + + def set_x(self, x: MPZ) -> Self: + self._x = x + return self + + def set_t(self, t: MPZ) -> Self: + self._t = t + return self + + def set_N(self, N: MPZ) -> Self: + self._N = N + return self + + def build(self) -> TimeLockPuzzle: + if self._x is None or self._t is None or self._N is None: + raise ValueError("All parameters (x, t, N) must be set before building") + return TimeLockPuzzle(self._x, self._t, self._N) diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py new file mode 100644 index 0000000..19380a2 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py @@ -0,0 +1,78 @@ +from typing import List, Tuple +import multiprocessing + +from src.time_lock_puzzle import TimeLockPuzzleBuilder +from ..mpc import MPC +from ..mpc.types import MPZ +from ..random import Random +from ..rsa.RSA import RSA +from .TimeLockPuzzle import TimeLockPuzzle +from .EfficientTimeLockPuzzleSolver import EfficientTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzleFactory import ITimeLockPuzzleFactory + + +class TimeLockPuzzleFactory(ITimeLockPuzzleFactory): + """Implementation of time lock puzzle factory.""" + + def __init__(self, bit_size: int, timing_parameter: MPZ) -> None: + """Initialize the factory. + + Args: + bit_size (int): Number of bits for RSA parameters + timing_parameter (MPZ): Time parameter t for puzzles + """ + self._bit_size = bit_size + self._t = timing_parameter + + def create_puzzle(self) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + # Create RSA instance + rsa_instance = RSA(self._bit_size) + + # Generate random x + rand = Random.get_random(self._bit_size) + x = MPC.mpz_urandomb(rand, self._bit_size) + + # Create puzzle using builder + puzzle = ( + TimeLockPuzzleBuilder() + .set_x(x) + .set_t(self._t) + .set_N(rsa_instance.get_N()) + .build() + ) + + # Get solution using efficient solver + y = EfficientTimeLockPuzzleSolver.solve(rsa_instance, puzzle) + + return puzzle, rsa_instance, y + + def create_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + # Create parameters for each puzzle + puzzle_params = [(self._bit_size, self._t) for _ in range(amount)] + + # Create puzzles in parallel using process pool + with multiprocessing.Pool() as pool: + puzzles = pool.map( + TimeLockPuzzleFactory._create_puzzle_parallel, puzzle_params + ) + + return puzzles + + # Private Methods + # ------------------------------------------------------------------------------ + + @staticmethod + def _create_puzzle_parallel( + puzzle_params: Tuple[int, MPZ], + ) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + """Helper method to create a single puzzle tuple for multiprocessing. + + Args: + puzzle_params (Tuple[int, MPZ]): Tuple containing (bit_size, timing_parameter) + + Returns: + Tuple[TimeLockPuzzle, RSA, MPZ]: A tuple containing the puzzle, RSA instance, and solution + """ + bit_size, t = puzzle_params + factory = TimeLockPuzzleFactory(bit_size, t) + return factory.create_puzzle() diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/__init__.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/__init__.py new file mode 100644 index 0000000..33481c8 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/__init__.py @@ -0,0 +1,25 @@ +"""Time lock puzzle module.""" + +from .TimeLockPuzzle import TimeLockPuzzle +from .TimeLockPuzzleBuilder import TimeLockPuzzleBuilder +from .TimeLockPuzzleFactory import TimeLockPuzzleFactory +from .EfficientTimeLockPuzzleSolver import EfficientTimeLockPuzzleSolver +from .SequentialTimeLockPuzzleSolver import SequentialTimeLockPuzzleSolver +from .abstract.ITimeLockPuzzle import ITimeLockPuzzle +from .abstract.ITimeLockPuzzleBuilder import ITimeLockPuzzleBuilder +from .abstract.ITimeLockPuzzleFactory import ITimeLockPuzzleFactory +from .abstract.IEfficientTimeLockPuzzleSolver import IEfficientTimeLockPuzzleSolver +from .abstract.ISequentialTimeLockPuzzleSolver import ISequentialTimeLockPuzzleSolver + +__all__ = [ + "TimeLockPuzzle", + "TimeLockPuzzleBuilder", + "TimeLockPuzzleFactory", + "EfficientTimeLockPuzzleSolver", + "SequentialTimeLockPuzzleSolver", + "ITimeLockPuzzle", + "ITimeLockPuzzleBuilder", + "ITimeLockPuzzleFactory", + "IEfficientTimeLockPuzzleSolver", + "ISequentialTimeLockPuzzleSolver", +] diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py new file mode 100644 index 0000000..f4b66df --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/IEfficientTimeLockPuzzleSolver.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple +from ...mpc.types import MPZ +from ...rsa import RSA +from .ITimeLockPuzzle import ITimeLockPuzzle + + +class IEfficientTimeLockPuzzleSolver(ABC): + """Abstract base class defining the interface for an efficient time lock puzzle solver.""" + + @staticmethod + @abstractmethod + def solve(rsa: RSA, puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle efficiently using RSA private parameters. + + Args: + rsa (RSA): The RSA instance with private parameters + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ + pass + + @staticmethod + @abstractmethod + def solve_many(puzzles: List[Tuple[RSA, ITimeLockPuzzle]]) -> List[MPZ]: + """Solve multiple time lock puzzles in parallel using multiprocessing. + + Args: + puzzles: List of tuples containing (RSA, puzzle) pairs to solve + + Returns: + List[MPZ]: List of solutions in the same order as input puzzles + """ + pass diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py new file mode 100644 index 0000000..c8f6889 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ISequentialTimeLockPuzzleSolver.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ +from .ITimeLockPuzzle import ITimeLockPuzzle + + +class ISequentialTimeLockPuzzleSolver(ABC): + """Abstract base class defining the interface for a sequential time lock puzzle solver.""" + + @staticmethod + @abstractmethod + def solve(puzzle: ITimeLockPuzzle) -> MPZ: + """Solve the time lock puzzle sequentially without RSA private parameters. + + Args: + puzzle (ITimeLockPuzzle): The puzzle to solve + + Returns: + MPZ: The solution y + """ diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py new file mode 100644 index 0000000..74b748d --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzle.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +from ...mpc.types import MPZ + + +class ITimeLockPuzzle(ABC): + """Abstract base class defining the interface for a time lock puzzle implementation.""" + + @abstractmethod + def get_x(self) -> MPZ: + """Get the input value x. + + Returns: + MPZ: The input value x + """ + + @abstractmethod + def get_t(self) -> MPZ: + """Get the time parameter t. + + Returns: + MPZ: The time parameter t + """ + + @abstractmethod + def get_N(self) -> MPZ: + """Get the modulus N. + + Returns: + MPZ: The modulus N + """ diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py new file mode 100644 index 0000000..aa24405 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleBuilder.py @@ -0,0 +1,49 @@ +from abc import ABC, abstractmethod +from typing import Self +from ...mpc.types import MPZ +from ..TimeLockPuzzle import TimeLockPuzzle + + +class ITimeLockPuzzleBuilder(ABC): + """Abstract base class defining the interface for a time lock puzzle builder.""" + + @abstractmethod + def set_x(self, x: MPZ) -> Self: + """Set the input value x. + + Args: + x (MPZ): The input value + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def set_t(self, t: MPZ) -> Self: + """Set the time parameter t. + + Args: + t (MPZ): The time parameter + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def set_N(self, N: MPZ) -> Self: + """Set the modulus N. + + Args: + N (MPZ): The modulus + + Returns: + ITimeLockPuzzleBuilder: The builder instance for chaining + """ + + @abstractmethod + def build(self) -> TimeLockPuzzle: + """Build the time lock puzzle. + + Returns: + TimeLockPuzzle: The constructed puzzle + """ diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py new file mode 100644 index 0000000..26bb910 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/ITimeLockPuzzleFactory.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple +from ...mpc.types import MPZ +from ...rsa import RSA +from ..TimeLockPuzzle import TimeLockPuzzle + + +class ITimeLockPuzzleFactory(ABC): + """Abstract base class defining the interface for a time lock puzzle factory.""" + + @abstractmethod + def create_puzzle(self) -> Tuple[TimeLockPuzzle, RSA, MPZ]: + """Create a new time lock puzzle with solution. + + Returns: + Tuple[TimeLockPuzzle, RSA, MPZ]: A tuple containing: + - The time lock puzzle + - The RSA instance used to create the puzzle + - The solution y + """ + + @abstractmethod + def create_puzzles(self, amount: int) -> List[Tuple[TimeLockPuzzle, RSA, MPZ]]: + """Create multiple time lock puzzles with solutions in parallel. + + Args: + amount (int): Number of puzzles to create + + Returns: + List[Tuple[TimeLockPuzzle, RSA, MPZ]]: A list of tuples, each containing: + - The time lock puzzle + - The RSA instance used to create the puzzle + - The solution y + """ diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orchestrator/puzzle-generator/src/time_lock_puzzle/constants.py b/orchestrator/puzzle-generator/src/time_lock_puzzle/constants.py new file mode 100644 index 0000000..acfd082 --- /dev/null +++ b/orchestrator/puzzle-generator/src/time_lock_puzzle/constants.py @@ -0,0 +1,5 @@ +"""Constants for time lock puzzle module.""" + +from ..mpc import MPC + +TWO = MPC.mpz(2) diff --git a/orchestrator/src/app.ts b/orchestrator/src/app.ts new file mode 100644 index 0000000..d511f33 --- /dev/null +++ b/orchestrator/src/app.ts @@ -0,0 +1,190 @@ +import Docker from 'dockerode'; +import { connectWithRetry, setupDatabase } from './db_tools.js'; +import Arweave from "arweave"; +import { getWalletAddress } from "./walletUtils"; +import { checkAndFetchIfNeeded, cleanupFulfilledEntries, crank, getProviderRequests, processChallengeRequests, processOutputRequests, gracefulShutdown } from './helperFunctions.js'; +import logger, { LogLevel, Logger } from './logger'; +import { monitoring } from './monitoring'; + +export const VERSION = "1.0.12"; + +export const docker = new Docker(); +export const DOCKER_NETWORK = process.env.DOCKER_NETWORK || "backend"; +export const TIME_PUZZLE_JOB_IMAGE = 'randao/puzzle-gen:v0.1.6'; +export const ORCHESTRATOR_IMAGE = 'randao/orchestrator:latest'; + +export const DOCKER_MONITORING_TIME = 30000; +export const POLLING_INTERVAL_MS = 0; //0 second +export const DATABASE_CHECK_TIME = 60000; //60 seconds +export const MINIMUM_ENTRIES = 10000; +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 = 50; +export const MINIMUM_RANDOM_DELTA = 25; +export const SHUTDOWN_POLLING_DELAY = 10; + +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)`); + + logger.debug(`\n[SKIPPED] Polling already in progress for ${lastPollingId}. Skipping this run.`); + logger.debug(`Completed steps so far: ${completedSteps.length > 0 ? completedSteps.join(", ") : "None"}`); + logger.verbose("Current step tracking status:", stepTracking); // Debugging info to inspect tracking object + return; // Prevent concurrent execution + } + + resetStepTracking(); // Reset step tracking for fresh polling + pollingInProgress = true; // Mark polling as in progress + const logId = getLogId(); + lastPollingId = logId; + logger.info(`${logId} Starting Polling...`); + + try { + const startTime = Date.now(); // Start time of polling + + // Step 1: Fetch open requests + const s1 = Date.now(); + logger.debug(`${logId} Step 1 started.`); + const openRequests = await getProviderRequests(PROVIDER_ID, logId); + const timeTaken = Date.now() - s1; + stepTracking.step1 = { completed: true, timeTaken }; + // Update monitoring with step timing + monitoring.updateStepTiming('step1', timeTaken); + logger.debug(`${logId} Step 1: Open requests fetched. Time taken: ${stepTracking.step1.timeTaken}ms`); + + // Run Step 2, 3, and 4 concurrently + await Promise.all([ + (async () => { + const s2 = Date.now(); + logger.debug(`${logId} Step 2 started.`); + await processChallengeRequests(client, openRequests.activeChallengeRequests, logId); + const timeTaken = Date.now() - s2; + stepTracking.step2 = { completed: true, timeTaken }; + // Update monitoring with step timing + monitoring.updateStepTiming('step2', timeTaken); + logger.debug(`${logId} Step 2 completed. Time taken: ${stepTracking.step2.timeTaken}ms`); + })(), + (async () => { + const s3 = Date.now(); + logger.debug(`${logId} Step 3 started.`); + await processOutputRequests(client, openRequests.activeOutputRequests, logId); + const timeTaken = Date.now() - s3; + stepTracking.step3 = { completed: true, timeTaken }; + // Update monitoring with step timing + monitoring.updateStepTiming('step3', timeTaken); + logger.debug(`${logId} Step 3 completed. Time taken: ${stepTracking.step3.timeTaken}ms`); + })(), + (async () => { + const s4 = Date.now(); + logger.debug(`${logId} Step 4 started.`); + //TODO enable this again later + await cleanupFulfilledEntries(client, openRequests, logId); + await checkAndFetchIfNeeded(client) + + crank(); //TODO find a better place for this + + const timeTaken = Date.now() - s4; + stepTracking.step4 = { completed: true, timeTaken }; + // Update monitoring with step timing + monitoring.updateStepTiming('step4', timeTaken); + logger.debug(`${logId} Step 4 completed. Time taken: ${stepTracking.step4.timeTaken}ms`); + })(), + ]); + + const totalTime = Date.now() - startTime; + // Update overall step timing in monitoring + monitoring.updateStepTiming('overall', totalTime); + logger.info(`${logId} Polling cycle completed successfully. Total time taken: ${totalTime}ms`); + + } catch (error) { + logger.error(`${logId} An error occurred during polling:`, error); + // Increment error count in monitoring + monitoring.incrementErrorCount(); + } finally { + pollingInProgress = false; // Reset flag after execution + } +} + +// Main function +async function run(): Promise { + logger.info("Orchestrator starting up"); + logger.debug("Environment variables loaded, initializing services"); + + const client = await connectWithRetry(); + await setupDatabase(client); + + // Initialize wallet and set provider ID using wallet utilities + getWalletAddress().then((address) => { + logger.info(`Provider ID: ${address}`); + PROVIDER_ID = address; + }).catch(error => { + logger.error('Failed to initialize wallet:', error); + process.exit(1); + }); + + // Handle graceful shutdown before entering infinite loop + process.on("SIGTERM", async () => { + logger.info("SIGTERM received. Shutting down gracefully."); + await client.end(); + await gracefulShutdown(); + for (let i = 0; i < SHUTDOWN_POLLING_DELAY; i++) { + try { + await polling(client); + } catch (error) { + logger.error(`Shutdown Polling iteration ${i + 1} failed:`, error); + } + } + await Logger.close(); // Use the static close method on the Logger class + process.exit(0); + }); + //TODO SEE WHATS BETTER (This could possibly have a new tx queed up while the old one is in the works to keep it speeds but who knows) + // setInterval(async () => { + // await polling(client); + // }, POLLING_INTERVAL_MS); + + // Infinite polling loop + while (true) { + try { + await polling(client); + } catch (error) { + logger.error("Polling error:", error); + } + } +} + + +run().catch((err) => logger.error(`Error in main function: ${err}`)); diff --git a/orchestrator/src/containerManagment.ts b/orchestrator/src/containerManagment.ts new file mode 100644 index 0000000..28588b8 --- /dev/null +++ b/orchestrator/src/containerManagment.ts @@ -0,0 +1,223 @@ +import { docker, DOCKER_NETWORK, MINIMUM_ENTRIES, TIME_PUZZLE_JOB_IMAGE } from "./app"; +import { dbConfig } from "./db_tools"; +import logger from "./logger"; + + +export interface NetworkConfig { + subnets: string[]; + securityGroups: string[]; +} + +// Global variables to track polling status +// Track which images have been pulled +let pulledDockerImage = false; +let pullingImagePromise: Promise | null = null; // Add at the top-level scope (module-global) +const ongoingContainers = new Set(); // Track container IDs of running Docker containers + +/** + * Pull a Docker image if it hasn't been pulled already + * @param imageName The name of the image to pull + * @returns A promise that resolves when the image has been pulled + */ +export async function pullDockerImage(imageName: string): Promise { + // Skip if already pulled + if (pulledDockerImage) { + logger.debug(`Image ${imageName} already pulled, skipping pull operation`); + return true; + } + + // If a pull is already in progress for this image, wait for it + if (pullingImagePromise) { + try { + await pullingImagePromise; + return true; + } catch (error) { + logger.error(`Failed to pull Docker image ${imageName}:`, error); + return false; + } + } + + // Start a new pull operation + pullingImagePromise = new Promise((resolve, reject) => { + logger.info(`Pulling image: ${imageName}`); + docker.pull(imageName, (err: Error | null, stream: NodeJS.ReadableStream | undefined) => { + if (err || !stream) { + pullingImagePromise = null; // reset on error + return false; + } + docker.modem.followProgress(stream, (doneErr: Error | null) => { + if (doneErr) { + pullingImagePromise = null; // reset on error + reject(doneErr); + } else { + pulledDockerImage = true; // Mark as pulled after success + resolve(); + } + }); + }); + }); + + try { + await pullingImagePromise; + return true; + } catch (error) { + logger.error(`Failed to pull Docker image:`, error); + pullingImagePromise = null; + return false; + } + } + +export async function triggerTimePuzzleJobPod(randomCount: number): Promise { + const containerName = `puzzle-gen_job_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + + if (ongoingContainers.size > 0) { + logger.debug("A puzzle-gen container is already running. Skipping new container launch."); + return null; + } + + // Ensure only one pull operation at a time + if (!pulledDockerImage) { + if (!pullingImagePromise) { + pullingImagePromise = new Promise((resolve, reject) => { + logger.info(`Pulling image: ${TIME_PUZZLE_JOB_IMAGE}`); + docker.pull(TIME_PUZZLE_JOB_IMAGE, (err: Error | null, stream: NodeJS.ReadableStream | undefined) => { + if (err || !stream) { + pullingImagePromise = null; // reset on error + return reject(err || new Error("Docker stream undefined")); + } + docker.modem.followProgress(stream, (doneErr: Error | null) => { + if (doneErr) { + pullingImagePromise = null; // reset on error + reject(doneErr); + } else { + pulledDockerImage = true; // Mark as pulled after success + resolve(); + } + }); + }); + }); + } + try { + await pullingImagePromise; + } catch (error) { + logger.error(`Failed to pull Docker image:`, error); + pullingImagePromise = null; + return null; + } + } + + logger.info(`Starting Docker container with name: ${containerName}`); + try { + const container = await docker.createContainer({ + Image: TIME_PUZZLE_JOB_IMAGE, + Cmd: ['sh', '-c', `python3 main.py ${randomCount}`], + Env: [ + `DATABASE_TYPE=postgresql`, + `DATABASE_HOST=${dbConfig.host}`, + `DATABASE_PORT=${dbConfig.port.toString()}`, + `DATABASE_USER=${dbConfig.user}`, + `DATABASE_PASSWORD=${dbConfig.password}`, + `DATABASE_NAME=${dbConfig.database}`, + ], + HostConfig: { + NetworkMode: DOCKER_NETWORK, + }, + name: containerName + }); + + await container.start(); + ongoingContainers.add(container.id); + logger.info(`Docker container ${containerName} started successfully.`); + return container.id; + } catch (error) { + logger.error(`Error starting Docker container ${containerName}:`, error); + return null; + } +} + +export async function getMoreRandom(randomToGenerate: number) { + logger.info(`Attempting to fetch ${randomToGenerate} random entries...`); + if (ongoingContainers.size > 0) { + logger.info("A puzzle-gen container is already running. Skipping new container launch."); + return null; + } + + logger.info(`Spawning a single container to generate ${randomToGenerate} random values.`); + + try { + const jobId = await triggerTimePuzzleJobPod(randomToGenerate); + if (jobId) { + logger.info(`Job triggered: ${jobId}`); + ongoingContainers.add(jobId); + return jobId; + } + } catch (error) { + logger.error('Error triggering job pod:', error); + } + + return null; +} + +// Import the function to reset the generation flag from helperFunctions +import { resetOngoingRandomGeneration } from './helperFunctions.js'; + +// Function to wait for Docker containers to complete and remove them from tracking +export async function monitorDockerContainers(): Promise { + if (ongoingContainers.size === 0){ + resetOngoingRandomGeneration(); + return; + } + + logger.verbose(`Monitoring ${ongoingContainers.size} Docker containers`); + let containersRemoved = false; + + for (const containerId of ongoingContainers) { + try { + const container = docker.getContainer(containerId); + const containerInfo = await container.inspect(); + + // Check if the container is already stopped (exited) + if (containerInfo.State.Status === 'exited') { + logger.debug(`Docker container stopped: ${containerId}`); + + // Attempt to remove the container, handling possible errors gracefully + try { + await container.remove({ force: true }); // Force removal to avoid "in progress" errors + logger.info(`Docker container removed: ${containerId}`); + ongoingContainers.delete(containerId); + containersRemoved = true; + + // Check exit code to log success or failure + if (containerInfo.State.ExitCode === 0) { + logger.info('Container completed successfully'); + } else { + logger.warn(`Container exited with non-zero code: ${containerInfo.State.ExitCode}`); + } + } catch (removeError) { + if (isDockerError(removeError) && removeError.statusCode === 409) { + // Error 409 means removal is in progress, so skip this container for now + logger.debug(`Removal of container ${containerId} is already in progress. Skipping.`); + } else { + // Handle other errors that might occur during container removal + logger.error(`Error removing Docker container ${containerId}:`, removeError); + } + } + } + } catch (error) { + logger.error(`Error inspecting Docker container ${containerId}:`, error); + ongoingContainers.delete(containerId); // Remove from tracking if there's an error (e.g., container not found) + containersRemoved = true; + } + } + + // If all containers are removed, reset the ongoingRandomGeneration flag + if (ongoingContainers.size === 0 && containersRemoved) { + logger.info('All random generation containers finished. Resetting generation flag.'); + resetOngoingRandomGeneration(); + } +} + +// Helper function to type guard Docker errors +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..6b9f082 --- /dev/null +++ b/orchestrator/src/db_tools.ts @@ -0,0 +1,83 @@ +import { Client } from "pg"; +import { MAX_RETRIES, RETRY_DELAY_MS } from "./app"; +import logger from "./logger"; + +export const dbConfig = { + host: process.env.DB_HOST || 'localhost', + 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 { + logger.info("Clearing database..."); + + await client.query(`SET session_replication_role = 'replica';`); + + const { rows } = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public';`); + for (const row of rows) { + logger.debug(`Dropping table: ${row.tablename}`); + await client.query(`DROP TABLE IF EXISTS "${row.tablename}" CASCADE;`); + } + + await client.query(`SET session_replication_role = 'origin';`); + + logger.info("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;`); + + logger.info("✅ Database setup complete or already exists."); + } catch (error: any) { + logger.error("❌ Legitimate issue encountered during database setup:", error.message); + } +} + +// Retry logic for connecting to PostgreSQL +export async function connectWithRetry(): Promise { + const client = new Client(dbConfig); + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + await client.connect(); + logger.info(`Connected to PostgreSQL database (Attempt ${attempt})`); + return client; + } catch (error) { + logger.warn(`Connection attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000} seconds...`); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + } + } + + throw new Error("Failed to connect to PostgreSQL after multiple attempts"); +} diff --git a/orchestrator/src/helperFunctions.ts b/orchestrator/src/helperFunctions.ts new file mode 100644 index 0000000..82f0360 --- /dev/null +++ b/orchestrator/src/helperFunctions.ts @@ -0,0 +1,914 @@ +import { GetOpenRandomRequestsResponse, GetProviderAvailableValuesResponse, RandomClient, RequestList } from "ao-process-clients"; +import { Client } from "pg"; +import { COMPLETION_RETENTION_PERIOD_MS, MINIMUM_ENTRIES, MINIMUM_RANDOM_DELTA, UNCHAIN_VS_OFFCHAIN_MAX_DIF, ORCHESTRATOR_IMAGE, docker } from "./app"; +import { getMoreRandom, monitorDockerContainers, pullDockerImage } from "./containerManagment"; +import logger, { LogLevel } from "./logger"; +import { monitoring } from "./monitoring"; +import { setTimeout, setInterval } from 'timers'; +import { getWallet } from "./walletUtils"; + +let randomClientInstance: RandomClient | null = null; +let lastInitTime: number = 0; +const REINIT_INTERVAL = 60 * 1 * 1000; // 1 minute in milliseconds +let current_onchain_random = - 10 + +// Cooldown tracking for updateAvailableValuesAsync +let lastUpdatedOnChainTime = 0; +const FIFTEEN_MINUTES_MS = 15 * 60 * 1000; + +// Cooldown tracking for fulfillRandomChallenge and fulfillRandomOutput (per request ID) +const challengeCooldowns = new Map(); +const outputCooldowns = new Map(); +// Track whether there's an ongoing random generation request +let ongoingRandomGeneration = false; +const MAX_RANDOM_PER_REQUEST = 500; // Maximum number of random values to generate in a single request + +// Map to track request timestamps +const requestTimestamps: Map = new Map(); + +let isInitializing = false; // Track if initialization is in progress +let initPromise: Promise | null = null; // Store the initialization promise + +// Function to reset the ongoingRandomGeneration flag +export function resetOngoingRandomGeneration() { + ongoingRandomGeneration = false; + logger.info('Random generation flag reset. System ready for new random generation requests.'); +} + +export async function getRandomClient(): Promise { + const currentTime = Date.now(); + + // Return the existing instance if it's valid and fresh + if (randomClientInstance && (currentTime - lastInitTime) <= REINIT_INTERVAL) { + return randomClientInstance; + } + + // If reinitialization is already happening, serve the old instance + if (isInitializing) { + logger.info('[RandomClient] Reinitialization in progress, serving old instance'); + return randomClientInstance!; + } + + // Start reinitialization in the background + isInitializing = true; + logger.info('[RandomClient] Background reinitialization triggered'); + + (async () => { + try { + // Use the wallet utilities to get the wallet (prioritizes SEED_PHRASE over WALLET_JSON) + const wallet = await getWallet(); + + const newClient = await (await RandomClient.defaultBuilder()) + .withWallet(wallet) + .withAOConfig({ + CU_URL: process.env.CU_URL || "https://ur-cu.randao.net", + MU_URL: process.env.MU_URL || "https://ur-mu.randao.net", + MODE: "legacy" as const + }) + .build(); + + // Swap instance only once it's ready + randomClientInstance = newClient; + lastInitTime = Date.now(); + logger.info('[RandomClient] Successfully reinitialized client'); + } catch (err) { + logger.error('[RandomClient] Reinitialization failed:', err); + } finally { + isInitializing = false; + } + })(); + + // Return the old instance immediately + return randomClientInstance!; +} + + +// Add a method to explicitly refresh the client if needed +export async function refreshRandomClient(): Promise { + randomClientInstance = null; + lastInitTime = 0; + return getRandomClient(); +} + +// Step 2: Process Challenge Requests (Database selection & assigning is atomic) +export async function processChallengeRequests( + client: Client, + activeChallengeRequests: { request_ids: string[] } | undefined, + parentLogId: string +): Promise { + logger.info(`${parentLogId} Step 2: Processing challenge requests.`); + + if (!activeChallengeRequests || activeChallengeRequests.request_ids.length === 0) { + logger.info(`${parentLogId} No Challenge Requests to process.`); + return; + } + + const requestIds = activeChallengeRequests.request_ids; + logger.info(`${parentLogId} Processing up to ${requestIds.length} requests.`); + + try { + await client.query('BEGIN'); // Start transaction + + logger.debug(`${parentLogId} Fetching existing request mappings.`); + + // Fetch already assigned request_id -> dbId mappings + const existingMappingsRes = await client.query( + `SELECT request_id FROM time_lock_puzzles + WHERE request_id = ANY($1) + FOR UPDATE SKIP LOCKED`, + [requestIds] + ); + + const existingRequestIds = new Set(existingMappingsRes.rows.map(row => row.request_id)); + logger.debug(`${parentLogId} Found ${existingRequestIds.size} already mapped requests.`); + + // Find only the unmapped requests (requestIds not in existingRequestIds) + const unmappedRequestIds = requestIds.filter(requestId => !existingRequestIds.has(requestId)); + logger.debug(`${parentLogId} Unmapped requests: ${unmappedRequestIds.length}`); + + let mappedEntries: { requestId: string, dbId: number }[] = []; + + if (unmappedRequestIds.length > 0) { + logger.debug(`${parentLogId} Fetching available DB entries.`); + const dbRes = await client.query( + `SELECT id FROM time_lock_puzzles + WHERE request_id IS NULL + ORDER BY id ASC + LIMIT $1 + FOR UPDATE SKIP LOCKED`, + [unmappedRequestIds.length] + ); + + const availableDbEntries = dbRes.rows.map(row => row.id); + logger.debug(`${parentLogId} Found ${availableDbEntries.length} available DB entries.`); + + if (availableDbEntries.length > 0) { + const numMappings = Math.min(unmappedRequestIds.length, availableDbEntries.length); + + for (let i = 0; i < numMappings; i++) { + await client.query( + `UPDATE time_lock_puzzles + SET request_id = $1 + WHERE id = $2`, + [unmappedRequestIds[i], availableDbEntries[i]] + ); + mappedEntries.push({ requestId: unmappedRequestIds[i], dbId: availableDbEntries[i] }); + logger.debug(`${parentLogId} Assigned Request ID ${unmappedRequestIds[i]} to DB Entry ${availableDbEntries[i]}.`); + } + } else { + logger.warn(`${parentLogId} No available DB entries for unmapped requests.`); + } + } + + // Collect all request IDs (previously mapped + newly mapped) + const allRequestIds = [...existingRequestIds, ...mappedEntries.map(entry => entry.requestId)]; + + if (allRequestIds.length === 0) { + logger.info(`${parentLogId} No requests to process. Committing transaction.`); + await client.query('COMMIT'); + return; + } + + await client.query('COMMIT'); // Commit all updates at once + logger.info(`${parentLogId} Committed all changes. Now fulfilling challenges.`); + + // Call fulfillRandomChallenge for all request IDs + await Promise.all( + allRequestIds.map(requestId => + fulfillRandomChallenge(client, requestId, parentLogId) + .catch(error => logger.error(`${parentLogId} Error fulfilling challenge for Request ID ${requestId}:`, error)) + ) + ); + + logger.info(`${parentLogId} All challenges fulfilled`); + } catch (error: any) { + logger.error(`${parentLogId} Error in processChallengeRequests:`, error); + await client.query('ROLLBACK'); // Rollback on failure + + logger.error(`SQL State: ${error.code}, Message: ${error.message}`); + } +} + +// Step 3: Process Output Requests +export async function processOutputRequests( + client: Client, + activeOutputRequests: { request_ids: string[] } | undefined, + parentLogId: string +): Promise { + logger.info(`${parentLogId} Step 3: Processing output requests.`); + + if (!activeOutputRequests || activeOutputRequests.request_ids.length === 0) { + logger.info(`${parentLogId} No Output Requests to process.`); + return; + } + + const outputPromises = activeOutputRequests.request_ids.map(async (requestId) => { + logger.debug(`${parentLogId} Processing output request ID: ${requestId}`); + + // Run fulfillRandomOutput asynchronously (do not await) + fulfillRandomOutput(client, requestId, parentLogId) + .catch(error => logger.error(`${parentLogId} Error fulfilling output:`, error)); + }); + + await Promise.all(outputPromises); + logger.info(`${parentLogId} Step 3 completed.`); +} + +// Step 4: Remove fulfilled entries no longer in use +export async function cleanupFulfilledEntries( + client: Client, + openRequests: any, + parentLogId: string +): Promise { + logger.info(`${parentLogId} Step 4: Checking for fulfilled entries no longer in use.`); + + const now = new Date(); + const cutoffTime = new Date(now.getTime() - COMPLETION_RETENTION_PERIOD_MS); + + try { + await client.query('BEGIN'); + + // Fetch all entries with a request_id + const result = await client.query(` + SELECT id, request_id, detected_completed + FROM time_lock_puzzles + WHERE request_id IS NOT NULL + `); + + let markForDeletion: string[] = []; + let markAsCompleted: string[] = []; + + for (const row of result.rows) { + const { id, request_id, detected_completed } = row; + + // Check if this request is still active in challenge or output + const isStillInChallenge = openRequests.activeChallengeRequests?.request_ids.includes(request_id); + const isStillInOutput = openRequests.activeOutputRequests?.request_ids.includes(request_id); + + if (!isStillInChallenge && !isStillInOutput) { + if (!detected_completed) { + // Mark it for deletion by setting detected_completed timestamp + markAsCompleted.push(id); + } else if (new Date(detected_completed) < cutoffTime) { + // If already marked and older than retention period, delete it + markForDeletion.push(id); + } + } + } + + // Mark entries as completed + if (markAsCompleted.length > 0) { + await client.query(` + UPDATE time_lock_puzzles + SET detected_completed = NOW() + WHERE id = ANY($1) + `, [markAsCompleted]); + logger.debug(`${parentLogId} Marked ${markAsCompleted.length} entries as completed.`); + } + + // Delete old completed entries + if (markForDeletion.length > 0) { + await client.query(` + DELETE FROM rsa_keys + WHERE id IN ( + SELECT rsa_id FROM time_lock_puzzles WHERE id = ANY($1) + ); + `, [markForDeletion]); + + await client.query(` + DELETE FROM time_lock_puzzles + WHERE id = ANY($1); + `, [markForDeletion]); + + logger.info(`${parentLogId} Deleted ${markForDeletion.length} old completed entries and corresponding RSA keys.`); + } + + await client.query('COMMIT'); + } catch (error) { + logger.error(`${parentLogId} Error in cleanupFulfilledEntries:`, error); + await client.query('ROLLBACK'); + } + + logger.info(`${parentLogId} Step 4 completed.`); +} + + +/** + * Function to log request timestamps + * Adds new request IDs to the tracking map and removes ones that are no longer present + * @param allRequestIds Array of request IDs to track + */ +export function logRequestTimestamps(allRequestIds: string[]): void { + const currentTime = Date.now(); + const existingIds = new Set(requestTimestamps.keys()); + + // Add new request IDs with current timestamp + for (const requestId of allRequestIds) { + if (!requestTimestamps.has(requestId)) { + logger.verbose(`Adding new request ID to tracking: ${requestId}`); + requestTimestamps.set(requestId, currentTime); + } + } + + // Remove request IDs that are no longer present + for (const existingId of existingIds) { + if (!allRequestIds.includes(existingId)) { + logger.verbose(`Removing request ID from tracking: ${existingId}`); + requestTimestamps.delete(existingId); + } + } +} + +// Function to check for defunct requests and crank if needed +export async function crank() { + const currentTime = Date.now(); + const defunctRequestIds: string[] = []; + const DEFUNCT_THRESHOLD_MS = 30 * 1000; // 30 seconds + + // Check for defunct request IDs (those that have been in the map for over 30 seconds) + requestTimestamps.forEach((timestamp, requestId) => { + const timeInMap = currentTime - timestamp; + if (timeInMap > DEFUNCT_THRESHOLD_MS) { + defunctRequestIds.push(requestId); + logger.info(`Defunct request found: ${requestId} (in system for ${Math.floor(timeInMap / 1000)} seconds)`); + } + }); + + // If there are any defunct requests, run the crank +// if (defunctRequestIds.length > 0) { +// logger.info(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); +// (await getRandomClient()).crank(); +// } else { +// // 1 in 100 chance to crank +// if (Math.floor(Math.random() * 100) === 0) { +// logger.info("Cranking randomly (1 in 100 chance hit)"); +// //(await getRandomClient()).crank(); +// } +// } +} + +export async function getProviderRequests(PROVIDER_ID: string, parentLogId: string): Promise { + const defaultResponse: GetOpenRandomRequestsResponse = { + providerId: PROVIDER_ID, + activeChallengeRequests: { request_ids: [] }, + activeOutputRequests: { request_ids: [] } + }; + + let client: RandomClient | null = null; + let response; + + try { + // Get client with error handling + try { + client = await getRandomClient(); + if (!client) { + throw new Error('Failed to initialize RandomClient'); + } + } catch (clientError) { + logger.error(`${parentLogId} Failed to initialize RandomClient:`, clientError); + return defaultResponse; + } + + // Try to get provider activity with retry logic + const maxRetries = 2; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + response = await client.getAllProviderActivity(); + lastError = null; + break; // Success, exit retry loop + } catch (error) { + lastError = error as Error; + logger.warn(`${parentLogId} Attempt ${attempt}/${maxRetries} failed to fetch provider activity:`, error); + + if (attempt < maxRetries) { + // Only recreate the client if this isn't the last attempt + try { + randomClientInstance = null; // Force client recreation on next attempt + client = await getRandomClient(); + logger.info(`${parentLogId} Recreated RandomClient for retry attempt ${attempt + 1}`); + } catch (retryError) { + logger.error(`${parentLogId} Failed to recreate RandomClient for retry:`, retryError); + } + // Wait before retrying + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + // If we still have an error after retries, handle it + if (lastError) { + throw lastError; + } + + if (!response) { + throw new Error('No response from provider activity'); + } + + // Collect all request IDs from all providers for tracking + const allRequestIds: string[] = []; + + // Process each provider to extract request IDs + for (const provider of response) { + try { + // Extract challenge request IDs + if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { + try { + const parsedChallengeData = JSON.parse(provider.active_challenge_requests); + if (parsedChallengeData && typeof parsedChallengeData === 'object' && 'request_ids' in parsedChallengeData) { + const requestIds = parsedChallengeData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + logger.warn(`${parentLogId} Warning: Failed to parse challenge requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + + // Extract output request IDs + if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { + try { + const parsedOutputData = JSON.parse(provider.active_output_requests); + if (parsedOutputData && typeof parsedOutputData === 'object' && 'request_ids' in parsedOutputData) { + const requestIds = parsedOutputData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + logger.warn(`${parentLogId} Warning: Failed to parse output requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + } catch (err) { + logger.warn(`${parentLogId} Warning: Failed to process provider ${provider?.provider_id || 'unknown'}:`, err); + } + } + + // Log all the request IDs for tracking + logger.debug(`${parentLogId} Found ${allRequestIds.length} request IDs across all providers`); + + // Update the request timestamps tracking + logRequestTimestamps(allRequestIds); + + const provider = response.find((p: any) => p.provider_id === PROVIDER_ID); + + if (!provider) { + logger.warn(`${parentLogId} Warning: Provider with ID ${PROVIDER_ID} not found.`); + return defaultResponse; + } + + // Attempt to parse fields if they exist, otherwise default to empty arrays + let parsedChallengeRequests: RequestList = { request_ids: [] }; + let parsedOutputRequests: RequestList = { request_ids: [] }; + + try { + if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { + const parsed = JSON.parse(provider.active_challenge_requests); + if (parsed && typeof parsed === 'object' && 'request_ids' in parsed) { + // Ensure we only include string values in the request_ids array + const validRequestIds = Array.isArray(parsed.request_ids) + ? parsed.request_ids.filter((id: any) => typeof id === 'string') + : []; + parsedChallengeRequests = { request_ids: validRequestIds }; + } + } + } catch (err) { + logger.warn(`${parentLogId} Warning: Failed to parse active_challenge_requests:`, err); + } + + try { + if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { + const parsed = JSON.parse(provider.active_output_requests); + if (parsed && typeof parsed === 'object' && 'request_ids' in parsed) { + // Ensure we only include string values in the request_ids array + const validRequestIds = Array.isArray(parsed.request_ids) + ? parsed.request_ids.filter((id: any) => typeof id === 'string') + : []; + parsedOutputRequests = { request_ids: validRequestIds }; + } + } + } catch (err) { + logger.warn(`${parentLogId} Warning: Failed to parse active_output_requests:`, err); + } + + // Only update current_onchain_random if successful + if (typeof provider.random_balance === 'number') { + current_onchain_random = provider.random_balance; + } + + const result: GetOpenRandomRequestsResponse = { + providerId: provider.provider_id || PROVIDER_ID, + activeChallengeRequests: parsedChallengeRequests, + activeOutputRequests: parsedOutputRequests, + }; + + logger.verbose(`${parentLogId} Step 1: Open Requests: ${JSON.stringify(result)}`); + logger.info(`${parentLogId} Step 1: Open Challenge Requests count: ${result.activeChallengeRequests.request_ids.length}`); + logger.info(`${parentLogId} Step 1: Open Output Requests count: ${result.activeOutputRequests.request_ids.length}`); + + return result; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`${parentLogId} Error in getProviderRequests:`, errorMessage); + logger.debug(`${parentLogId} Error details:`, error); + + // If we have a client that might be in a bad state, try to clean it up + if (client) { + try { + randomClientInstance = null; + } catch (cleanupError) { + logger.warn(`${parentLogId} Error during client cleanup:`, cleanupError); + } + } + + return defaultResponse; + } +} + +// Function to check and fetch database entries as needed +export async function checkAndFetchIfNeeded(client: Client) { + try { + // Query current count of usable DB entries + const res = await client.query( + 'SELECT COUNT(*) AS count FROM time_lock_puzzles WHERE request_id IS NULL' + ); + const currentCount = parseInt(res.rows[0].count, 10); + logger.info(`Total usable DB entries: ${currentCount}`); + + switch (current_onchain_random) { + case -1: + logger.warn("Value is -1"); + logger.warn("Provider has been shut down by USER..."); + logger.warn("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-1); + break; + case -2: + logger.error("Value is -2"); + logger.error("Provider has been shut down by PROCESS..."); + logger.error("This is due to One of the following: "); + logger.error("Failing to provide random fast enough (Provider is not responding to random requests and considered unhealthy NO SLASH)"); + logger.error("Failing to provide the proof for an outstanding random request within the time. (Provider was likely turned off mid random request SMALL SLASH)"); + logger.error("Failing to provide the correct proof for your original random (Provider was detected as malicious for tampering with the random LARGE SLASH)"); + logger.error("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-2); + break; + case -3: + logger.warn("Value is -3"); + logger.warn("Provider has been shut down by PROCESS..."); + logger.warn("This was likely done as a test or to get the maintainers attention. Contact team if you see this and are not sure why"); + logger.warn("Go to the provider dashboard to turn back on"); + await updateAvailableValuesAsync(-3); + break; + case -4: + logger.warn("Value is -4"); + logger.warn("Nothing Set up for this yet"); + await updateAvailableValuesAsync(-4); + break; + case -5: + logger.warn("Value is -5"); + logger.warn("Provider has been told to pull the latest image and restart. Taking action now"); + + // Pull the randomrequester image + logger.info(`Attempting to pull Docker image: ${ORCHESTRATOR_IMAGE}`); + const pullSuccess = await pullDockerImage(ORCHESTRATOR_IMAGE); + + if (pullSuccess) { + logger.info(`Successfully pulled image: ${ORCHESTRATOR_IMAGE}`); + } else { + logger.error(`Failed to pull image: ${ORCHESTRATOR_IMAGE}`); + } + + // Regardless of pull result, proceed with shutdown and restart + await gracefulShutdown(); + const container = docker.getContainer(process.env.HOSTNAME || ""); // or use docker ps/inspect to get container ID + await container.remove({ force: true }); + process.exit(1); + break; + case -10: + logger.info("Value is -10"); + logger.info("Provider has been turned on and is starting up OR is not staked yet"); + logger.info("Go to the provider dashboard to Stake if you have not yet OR wait for provider to finish turning on if you have staked already"); + break; + default: + logger.debug("Provider is up and working"); + logger.debug(`Onchain Value is ${current_onchain_random}`); + logger.debug(`Local Value is ${currentCount}`); + await updateAvailableValuesAsync(currentCount); + + } + + // First check if a random generation is already in progress. If so, see if it's done + if (ongoingRandomGeneration) { + logger.info('A random generation process is already running. Skipping new request.'); + await monitorDockerContainers(); + return; + } + + // Check if more entries are needed + const entriesNeeded = MINIMUM_ENTRIES - currentCount; + if (entriesNeeded < MINIMUM_RANDOM_DELTA) return; + + // Set the flag to prevent concurrent random generation + ongoingRandomGeneration = true; + + // Limit to MAX_RANDOM_PER_REQUEST + const randomToGenerate = Math.min(entriesNeeded, MAX_RANDOM_PER_REQUEST); + + logger.info(`Less than ${MINIMUM_ENTRIES} entries found. Fetching ${randomToGenerate} entries (out of ${entriesNeeded} needed)...`); + + // Start the random generation with the calculated amount + await getMoreRandom(randomToGenerate); + + } catch (error) { + logger.error('Error during check and fetch:', error); + ongoingRandomGeneration = false; // Reset the flag on error + } +} + +export async function updateAvailableValuesAsync(currentCount: number) { + const now = Date.now(); + + if (now - lastUpdatedOnChainTime < FIFTEEN_MINUTES_MS) { + logger.info(`On-chain update skipped - only ${Math.floor((now - lastUpdatedOnChainTime) / 1000)}s since last update`); + return; + } + + try { + const monitoringData = await monitoring.getMonitoringData(); + + await (await getRandomClient()).updateProviderAvailableValues(currentCount, monitoringData); + logger.info(`Updated provider values to ${currentCount}`); + + lastUpdatedOnChainTime = now; + } catch (error) { + logger.error("Failed to update provider values:", error); + monitoring.incrementErrorCount(); + } +} + +// Function to post VDF challenge (fetches dbId dynamically) +async function fulfillRandomChallenge(client: Client, requestId: string, parentLogId: string): Promise { + const logPrefix = `${parentLogId} [Challenge ${requestId}]`; + + // Check if this request is already being processed + if (challengeCooldowns.has(requestId)) { + logger.debug(`${logPrefix} Request is in cooldown, skipping...`); + return; + } + + // Set cooldown immediately to prevent concurrent processing + challengeCooldowns.set(requestId, true); + const cooldownTimer = setTimeout(() => { + challengeCooldowns.delete(requestId); + logger.debug(`${logPrefix} Cooldown released`); + }, 60000); // 1 minute cooldown + + let randomClient: RandomClient | null = null; + let retryCount = 0; + const maxRetries = 3; + const baseDelay = 1000; // 1 second base delay + + while (retryCount <= maxRetries) { + try { + // Get a fresh client for each attempt + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient'); + } + + // Fetch the necessary details from the database using requestId + const res = await client.query( + `SELECT id, modulus, x + FROM time_lock_puzzles + WHERE request_id = $1`, + [requestId] + ); + + if (!res.rowCount) { + logger.error(`${logPrefix} No database entry found for request`); + return; + } + + + const { id: dbId, modulus, x: input } = res.rows[0]; + + if (!modulus || !input) { + throw new Error('Missing required fields in database entry'); + } + + logger.debug(`${logPrefix} Posting VDF challenge for DB ID: ${dbId}`); + + // Post the VDF challenge + await randomClient.commit({ + requestId: requestId, + puzzle: { + input: input, + modulus: modulus + } + }); + + logger.info(`${logPrefix} Successfully posted VDF challenge`); + return; // Success, exit the function + + } catch (error) { + retryCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + + if (retryCount <= maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff + logger.warn(`${logPrefix} Attempt ${retryCount}/${maxRetries} failed, retrying in ${delay}ms:`, errorMessage); + + // Force client recreation on retry + randomClient = null; + randomClientInstance = null; + + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + logger.error(`${logPrefix} All ${maxRetries} attempts failed:`, error); + // The cooldown was already set at the beginning + return; + } + } finally { + // Ensure the client is properly cleaned up + if (randomClient) { + try { + if (typeof (randomClient as any).disconnect === 'function') { + await (randomClient as any).disconnect().catch((e: Error) => + logger.warn(`${logPrefix} Error disconnecting client:`, e) + ); + } + } catch (e) { + logger.warn(`${logPrefix} Error during client cleanup:`, e); + } + } + } + } +} + +// Function to post VDF output and proof +async function fulfillRandomOutput(client: Client, requestId: string, parentLogId: string): Promise { + const logPrefix = `${parentLogId} [Output ${requestId}]`; + + // Check if this request is already being processed + if (outputCooldowns.has(requestId)) { + logger.debug(`${logPrefix} Request is in cooldown, skipping...`); + return; + } + + // Set cooldown immediately to prevent concurrent processing + outputCooldowns.set(requestId, true); + const cooldownTimer = setTimeout(() => { + outputCooldowns.delete(requestId); + logger.debug(`${logPrefix} Cooldown released`); + }, 60000); // 1 minute cooldown + + let randomClient: RandomClient | null = null; + let retryCount = 0; + const maxRetries = 3; + const baseDelay = 1000; // 1 second base delay + + while (retryCount <= maxRetries) { + try { + // Get a fresh client for each attempt + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient'); + } + + // Fetch the output and proof from the database using the requestId + const res = await client.query( + `SELECT + tlp.id, + tlp.y AS output, + rk.p, + rk.q + FROM time_lock_puzzles tlp + JOIN rsa_keys rk ON tlp.rsa_id = rk.id + WHERE tlp.request_id = $1`, + [requestId] + ); + + if (!res.rowCount) { + logger.error(`${logPrefix} No database entry found for request`); + return; + } + + + const { id: dbId, output, p: rsaP, q: rsaQ } = res.rows[0]; + + if (!output || !rsaP || !rsaQ) { + throw new Error('Missing required fields in database entry'); + } + + logger.debug(`${logPrefix} Posting VDF output and proof for DB ID: ${dbId}`); + + // Post the VDF output and proof + await randomClient.reveal({ + requestId: requestId, + rsa_key: { + p: rsaP, + q: rsaQ + } + }); + + logger.info(`${logPrefix} Successfully posted VDF output and proof`); + return; // Success, exit the function + + } catch (error) { + retryCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + + if (retryCount <= maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff + logger.warn(`${logPrefix} Attempt ${retryCount}/${maxRetries} failed, retrying in ${delay}ms:`, errorMessage); + + // Force client recreation on retry + randomClient = null; + randomClientInstance = null; + + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + logger.error(`${logPrefix} All ${maxRetries} attempts failed:`, error); + // The cooldown was already set at the beginning + return; + } + } finally { + // Ensure the client is properly cleaned up + if (randomClient) { + try { + if (typeof (randomClient as any).disconnect === 'function') { + await (randomClient as any).disconnect().catch((e: Error) => + logger.warn(`${logPrefix} Error disconnecting client:`, e) + ); + } + } catch (e) { + logger.warn(`${logPrefix} Error during client cleanup:`, e); + } + } + } + } +} + +export async function gracefulShutdown() { + const logPrefix = '[Shutdown]'; + let randomClient: RandomClient | null = null; + + try { + logger.info(`${logPrefix} Starting graceful shutdown sequence`); + + // Get monitoring data for final update + const monitoringData = await monitoring.getMonitoringData(); + + // Get a fresh client for the shutdown sequence + randomClient = await getRandomClient(); + if (!randomClient) { + throw new Error('Failed to initialize RandomClient during shutdown'); + } + + // Set provider available values to 0 and include final monitoring data + logger.info(`${logPrefix} Updating provider values to 0...`); + await randomClient.updateProviderAvailableValues(0, monitoringData); + + logger.info(`${logPrefix} Provider values updated to 0`); + + // Add a small delay to ensure the update is processed + await new Promise(resolve => setTimeout(resolve, 2000)); + + logger.info(`${logPrefix} Shutdown sequence completed`); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`${logPrefix} Error during shutdown:`, errorMessage); + logger.debug(`${logPrefix} Error details:`, error); + monitoring.incrementErrorCount(); + } finally { + // // Ensure the client is properly cleaned up + // if (randomClient) { + // try { + // if (typeof (randomClient as any).disconnect === 'function') { + // await (randomClient as any).disconnect().catch((e: Error) => + // logger.warn(`${logPrefix} Error disconnecting client:`, e) + // ); + // } + // } catch (e) { + // logger.warn(`${logPrefix} Error during client cleanup:`, e); + // } + // } + + // // Clear the client instance to ensure a fresh start if the process continues + // randomClientInstance = null; + + + } +} diff --git a/orchestrator/src/logger.ts b/orchestrator/src/logger.ts new file mode 100644 index 0000000..8db7fdc --- /dev/null +++ b/orchestrator/src/logger.ts @@ -0,0 +1,254 @@ +import fs from 'fs'; +import path from 'path'; + +// Enum for different log levels +export enum LogLevel { + SILENT = 0, // No logging + ERROR = 1, // Only errors + WARN = 2, // Errors and warnings + INFO = 3, // Normal operational logs (default) + DEBUG = 4, // More detailed information + VERBOSE = 5 // Everything including detailed debugging +} + +// Log level names for better readability +const LogLevelNames: Record = { + [LogLevel.SILENT]: 'SILENT', + [LogLevel.ERROR]: 'ERROR', + [LogLevel.WARN]: 'WARN', + [LogLevel.INFO]: 'INFO', + [LogLevel.DEBUG]: 'DEBUG', + [LogLevel.VERBOSE]: 'VERBOSE' +}; + +export interface LoggerConfig { + consoleLogLevel: LogLevel; + fileLogLevel: LogLevel; + logFilePath: string; + maxLogFileSizeBytes: number; + rotateLogFiles: boolean; + maxLogFiles: number; +} + +export class Logger { + private static instance: Logger; + private config: LoggerConfig; + private logStream: fs.WriteStream | null = null; + + private constructor(config: LoggerConfig) { + this.config = config; + this.setupLogStream(); + this.logToFile(LogLevel.INFO, `Logger initialized with console level: ${LogLevelNames[config.consoleLogLevel]}, file level: ${LogLevelNames[config.fileLogLevel]}`); + } + + private setupLogStream(): void { + try { + // Create directory if it doesn't exist + const logDir = path.dirname(this.config.logFilePath); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + // Check if file exists and needs rotation + if (this.config.rotateLogFiles && fs.existsSync(this.config.logFilePath)) { + const stats = fs.statSync(this.config.logFilePath); + if (stats.size >= this.config.maxLogFileSizeBytes) { + this.rotateLogFiles(); + } + } + + // Create or open the log file + this.logStream = fs.createWriteStream(this.config.logFilePath, { flags: 'a' }); + + // Handle errors on the stream + this.logStream.on('error', (err) => { + console.error(`Error writing to log file: ${err}`); + }); + } catch (error) { + console.error(`Failed to setup log file: ${error}`); + } + } + + private rotateLogFiles(): void { + try { + for (let i = this.config.maxLogFiles - 1; i > 0; i--) { + const oldFile = `${this.config.logFilePath}.${i - 1}`; + const newFile = `${this.config.logFilePath}.${i}`; + + if (fs.existsSync(oldFile)) { + if (fs.existsSync(newFile)) { + fs.unlinkSync(newFile); + } + fs.renameSync(oldFile, newFile); + } + } + + const oldestFile = `${this.config.logFilePath}.0`; + if (fs.existsSync(this.config.logFilePath)) { + if (fs.existsSync(oldestFile)) { + fs.unlinkSync(oldestFile); + } + fs.renameSync(this.config.logFilePath, oldestFile); + } + } catch (error) { + console.error(`Failed to rotate log files: ${error}`); + } + } + + private formatLogEntry(level: LogLevel, message: string, ...args: any[]): string { + const timestamp = new Date().toISOString(); + const levelName = LogLevelNames[level]; + + // Format any objects in the args array + const formattedArgs = args.map(arg => { + if (typeof arg === 'object' && arg !== null) { + try { + return JSON.stringify(arg); + } catch (e) { + return String(arg); + } + } + return String(arg); + }); + + return `[${timestamp}] [${levelName}] ${message} ${formattedArgs.join(' ')}`.trim(); + } + + private logToConsole(level: LogLevel, message: string, ...args: any[]): void { + if (level <= this.config.consoleLogLevel) { + const formattedMessage = this.formatLogEntry(level, message, ...args); + + switch (level) { + case LogLevel.ERROR: + console.error(formattedMessage); + break; + case LogLevel.WARN: + console.warn(formattedMessage); + break; + default: + console.log(formattedMessage); + break; + } + } + } + + private logToFile(level: LogLevel, message: string, ...args: any[]): void { + if (this.logStream && level <= this.config.fileLogLevel) { + try { + const formattedMessage = this.formatLogEntry(level, message, ...args); + this.logStream.write(formattedMessage + '\n'); + } catch (error) { + console.error(`Failed to write to log file: ${error}`); + } + } + } + + public log(level: LogLevel, message: string, ...args: any[]): void { + this.logToConsole(level, message, ...args); + this.logToFile(level, message, ...args); + } + + public error(message: string, ...args: any[]): void { + this.log(LogLevel.ERROR, message, ...args); + } + + public warn(message: string, ...args: any[]): void { + this.log(LogLevel.WARN, message, ...args); + } + + public info(message: string, ...args: any[]): void { + this.log(LogLevel.INFO, message, ...args); + } + + public debug(message: string, ...args: any[]): void { + this.log(LogLevel.DEBUG, message, ...args); + } + + public verbose(message: string, ...args: any[]): void { + this.log(LogLevel.VERBOSE, message, ...args); + } + + // Static methods for singleton pattern + public static getInstance(): Logger { + if (!Logger.instance) { + Logger.initialize(); + } + return Logger.instance; + } + + public static initialize(config?: Partial): Logger { + // Default configuration + const defaultConfig: LoggerConfig = { + consoleLogLevel: this.parseLogLevel(process.env.LOG_CONSOLE_LEVEL) || LogLevel.INFO, + fileLogLevel: this.parseLogLevel(process.env.LOG_FILE_LEVEL) || LogLevel.VERBOSE, + logFilePath: process.env.LOG_FILE_PATH || path.join(process.cwd(), 'logs', 'orchestrator.log'), + maxLogFileSizeBytes: parseInt(process.env.LOG_MAX_SIZE || '10485760', 10), // 10MB default + rotateLogFiles: process.env.LOG_ROTATE === 'true', + maxLogFiles: parseInt(process.env.LOG_MAX_FILES || '5', 10) + }; + + // Merge with provided configuration + const mergedConfig = { ...defaultConfig, ...config }; + + if (Logger.instance) { + // Update configuration if instance already exists + Logger.instance.config = mergedConfig; + Logger.instance.logStream?.end(); + Logger.instance.setupLogStream(); + } else { + Logger.instance = new Logger(mergedConfig); + } + + return Logger.instance; + } + + // Utility to parse log level from string + private static parseLogLevel(level?: string): LogLevel | undefined { + if (!level) return undefined; + + // Try to parse numeric value + const numericLevel = parseInt(level, 10); + if (!isNaN(numericLevel) && numericLevel >= 0 && numericLevel <= 5) { + return numericLevel as LogLevel; + } + + // Parse string values + switch (level.toUpperCase()) { + case 'SILENT': return LogLevel.SILENT; + case 'ERROR': return LogLevel.ERROR; + case 'WARN': return LogLevel.WARN; + case 'INFO': return LogLevel.INFO; + case 'DEBUG': return LogLevel.DEBUG; + case 'VERBOSE': return LogLevel.VERBOSE; + default: return undefined; + } + } + + // Helper to update log level dynamically + public static setLogLevel(consoleLevel?: LogLevel, fileLevel?: LogLevel): void { + const instance = Logger.getInstance(); + if (consoleLevel !== undefined) { + instance.config.consoleLogLevel = consoleLevel; + } + if (fileLevel !== undefined) { + instance.config.fileLogLevel = fileLevel; + } + } + + // Close logger (for graceful shutdown) + public static close(): Promise { + return new Promise((resolve) => { + if (Logger.instance && Logger.instance.logStream) { + Logger.instance.logStream.end(() => { + Logger.instance.logStream = null; + resolve(); + }); + } else { + resolve(); + } + }); + } +} + +// Export default instance for convenience +export default Logger.getInstance(); diff --git a/orchestrator/src/monitoring.ts b/orchestrator/src/monitoring.ts new file mode 100644 index 0000000..5d847dd --- /dev/null +++ b/orchestrator/src/monitoring.ts @@ -0,0 +1,272 @@ +import os from 'os'; +import fs from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import crypto from 'crypto'; +import logger from './logger'; +import { MonitoringData, PerformanceMetrics, SystemSpecs, ExecutionMetrics, HealthStatus } from 'ao-process-clients'; +import { VERSION } from './app'; + +const execAsync = promisify(exec); + +// Moving averages for each step +interface StepTimings { + step1: number; + step2: number; + step3: number; + step4: number; + overall: number; +} + +// Class to manage all monitoring data +export class MonitoringService { + private static instance: MonitoringService; + private machineId: string; + + // Metrics tracking + private stepTimings: StepTimings = { + step1: 0, + step2: 0, + step3: 0, + step4: 0, + overall: 0 + }; + + private totalStepSamples: { [key: string]: number } = { + step1: 0, + step2: 0, + step3: 0, + step4: 0, + overall: 0 + }; + + private errorCount: number = 0; + private errorTimestamps: number[] = []; // store Unix timestamps + + // Network monitoring properties + private static previousNetworkBytes = { rx: 0, tx: 0 }; + private static lastCheckTime = Date.now(); + + private constructor() { + this.machineId = this.generateMachineId(); + + // Initialize network stats + this.updateNetworkStats().catch(err => + logger.error('Failed to initialize network stats:', err) + ); + } + + public static getInstance(): MonitoringService { + if (!MonitoringService.instance) { + MonitoringService.instance = new MonitoringService(); + } + return MonitoringService.instance; + } + + private generateMachineId(): string { + try { + // Using stable hardware identifiers that won't change between restarts + // but will be unique to physical/virtual machines + + // Get CPU information which is generally the same across containers on same host + const cpuModel = os.cpus()[0]?.model || ''; + const cpuSpeed = os.cpus()[0]?.speed || 0; + const totalCores = os.cpus().length; + // System memory size is usually fixed for a machine + const totalMemory = os.totalmem(); + + // Combine all available identifiers with more hardware specs + const hwInfo = `${cpuModel}-${cpuSpeed}-${totalCores}-${totalMemory}-${os.platform()}-${os.arch()}`; + + // Generate a shorter hash (first 16 chars of SHA-256) for easier identification while maintaining uniqueness + return crypto.createHash('sha256').update(hwInfo).digest('hex').substring(0, 16); + } catch (error) { + logger.error('Error generating machine ID:', error); + // Fallback to a null entry for those who care + return '0000000000000000'; + } + } + + private async updateNetworkStats(): Promise<{ rx_sec: number; tx_sec: number }> { + try { + // Store current and previous bytes for rate calculation + const currentBytes = { + rx: 0, + tx: 0 + }; + + // Initialize network stats with default values + const networkStats: { rx_sec: number; tx_sec: number } = { + rx_sec: 0, + tx_sec: 0 + }; + + if (process.platform === 'linux') { + // Linux - read from /proc/net/dev + const netDev = await fs.promises.readFile('/proc/net/dev', 'utf8'); + const interfaces = netDev.split('\n').filter(line => + line.includes(':') && !line.includes('lo:') + ); + + for (const intf of interfaces) { + const parts = intf.trim().split(/\s+/); + currentBytes.rx += parseInt(parts[1] || '0', 10); + currentBytes.tx += parseInt(parts[9] || '0', 10); + } + } else if (process.platform === 'win32') { + // Windows - use PowerShell to get network stats + const { stdout } = await execAsync( + 'powershell "Get-NetAdapterStatistics | Select-Object ReceivedBytes,SentBytes | ConvertTo-Json"' + ); + + try { + const stats = JSON.parse(stdout); + const adapters = Array.isArray(stats) ? stats : [stats]; + for (const adapter of adapters) { + currentBytes.rx += adapter.ReceivedBytes || 0; + currentBytes.tx += adapter.SentBytes || 0; + } + } catch (e) { + logger.error('Failed to parse network stats:', e); + } + } + + // Calculate rates in bytes per second + const now = Date.now(); + const timeDiffSeconds = (now - MonitoringService.lastCheckTime) / 1000; + + if (timeDiffSeconds > 0 && MonitoringService.previousNetworkBytes.rx > 0) { + // Calculate rates only if we have previous measurements + networkStats.rx_sec = Math.max(0, (currentBytes.rx - MonitoringService.previousNetworkBytes.rx) / timeDiffSeconds); + networkStats.tx_sec = Math.max(0, (currentBytes.tx - MonitoringService.previousNetworkBytes.tx) / timeDiffSeconds); + } + + // Store current values for next calculation + MonitoringService.previousNetworkBytes = { ...currentBytes }; + MonitoringService.lastCheckTime = now; + + return networkStats; + } catch (error) { + logger.error('Error getting network stats:', error); + return { + rx_sec: 0, + tx_sec: 0 + }; + } + } + + public updateStepTiming(step: string, timeTaken: number): void { + if (step in this.stepTimings) { + // Calculate running average + const currentSamples = this.totalStepSamples[step]; + const currentAvg = this.stepTimings[step as keyof StepTimings]; + + // Update running average + this.stepTimings[step as keyof StepTimings] = + (currentAvg * currentSamples + timeTaken) / (currentSamples + 1); + this.totalStepSamples[step]++; + } + } + + public incrementErrorCount(): void { + this.errorCount++; + const now = Date.now(); + this.errorTimestamps.push(now); + } + + private countErrorsSince(msAgo: number): number { + const cutoff = Date.now() - msAgo; + return this.errorTimestamps.filter(ts => ts >= cutoff).length; + } + + private cleanupOldErrors(): void { + const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; + this.errorTimestamps = this.errorTimestamps.filter(ts => ts >= oneDayAgo); + } + + public async getMonitoringData(): Promise { + // Get real-time system metrics + const cpuInfo = os.cpus(); + const loadAvg = os.loadavg(); + const totalMemory = os.totalmem(); + const freeMemory = os.freemem(); + const usedMemoryPercent = Math.round((1 - freeMemory / totalMemory) * 100); + + // Get disk info - only used percent + let diskUsedPercent = 0; + + try { + // Cleanup old errors to reduce memory + this.cleanupOldErrors(); + + if (process.platform === 'linux') { + const { stdout } = await execAsync('df -h / --output=pcent'); + const lines = stdout.trim().split('\n'); + if (lines.length > 1) { + diskUsedPercent = parseInt(lines[1].trim().replace('%', ''), 10); + } + } else if (process.platform === 'win32') { + const { stdout } = await execAsync( + 'powershell "Get-Volume | Where-Object {$_.DriveLetter -eq \'C\'} | Select-Object @{Name=\'UsedPercent\';Expression={100 - (($_.SizeRemaining / $_.Size) * 100)}} | ConvertTo-Json"' + ); + + try { + const diskData = JSON.parse(stdout); + diskUsedPercent = Math.round(diskData.UsedPercent || 0); + } catch (e) { + logger.error('Failed to parse disk info:', e); + } + } + } catch (error) { + logger.error('Error getting disk info:', error); + } + + // Get updated network stats + const networkStats = await this.updateNetworkStats(); + + // Create the SystemSpecs object + const systemSpecs: SystemSpecs = { + arch: os.arch(), + uptime: os.uptime(), + cpuCount: cpuInfo.length, + memoryTotalBytes: totalMemory, + token: this.machineId + }; + + // Create the PerformanceMetrics object + const performance: PerformanceMetrics = { + loadAverage: loadAvg, + memoryUsedPercent: usedMemoryPercent, + diskUsedPercent: diskUsedPercent, + network: networkStats + }; + + // Create the ExecutionMetrics object + const executionMetrics: ExecutionMetrics = { + stepTimingsMs: this.stepTimings as unknown as Record + }; + + // Create the HealthStatus object + const health: HealthStatus = { + errorTotal: this.errorCount, + errorsLastHour: this.countErrorsSince(60 * 60 * 1000), + errorsLastDay: this.countErrorsSince(24 * 60 * 60 * 1000), + status: this.countErrorsSince(60 * 60 * 1000) > 10 ? "degraded" : "healthy" + }; + + // Construct the full MonitoringData object + const monitoringData: MonitoringData = { + providerVersion: VERSION, + timestamp: new Date().toISOString(), + systemSpecs, + performance, + executionMetrics, + health + }; + + return monitoringData; + } +} + +// Export a singleton instance +export const monitoring = MonitoringService.getInstance(); 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..3629f5e --- /dev/null +++ b/orchestrator/src/reset_db.ts @@ -0,0 +1,51 @@ +import { Client } from 'pg'; +import { dbConfig } from './db_tools'; +import logger from './logger'; + +interface TableRow { + tablename: string; +} + +// Function to connect to the database and drop all tables +async function resetDatabase(): Promise { + logger.info("Connecting to PostgreSQL to reset the database..."); + + const client = new Client(dbConfig); + try { + await client.connect(); + logger.info("Connected to database. Dropping all tables..."); + + // Disable foreign key constraints (important for dropping tables safely) + await client.query(`SET session_replication_role = 'replica';`); + + // 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) { + logger.info("No tables found in the database."); + } else { + // Drop each table + for (const table of tables) { + logger.info(`Dropping table: ${table}`); + await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE;`); + } + logger.info("All tables dropped successfully."); + } + + // Re-enable foreign key constraints + await client.query(`SET session_replication_role = 'origin';`); + + } catch (error) { + logger.error("Error while resetting database:", error); + } finally { + await client.end(); + logger.info("Database connection closed."); + } +} + +// Run the reset function +resetDatabase().catch(error => logger.error("Failed to reset database:", error)); diff --git a/orchestrator/src/walletUtils.ts b/orchestrator/src/walletUtils.ts new file mode 100644 index 0000000..2a0b23a --- /dev/null +++ b/orchestrator/src/walletUtils.ts @@ -0,0 +1,240 @@ +import { getKeyPairFromSeed } from "human-crypto-keys"; +import type { JWKInterface } from "arweave/web/lib/wallet"; +import { passwordStrength } from "check-password-strength"; +import { isOneOf, isString } from "typed-assert"; +import { wordlists, mnemonicToSeed } from "bip39-web-crypto"; +import logger from "./logger"; +import Arweave from "arweave"; +import * as fs from 'fs/promises'; + +// --- Configuration --- + +// ⭐ Recommendation: Externalize Arweave configuration for flexibility. +const arweave = Arweave.init({ + host: process.env.ARWEAVE_HOST || "arweave.net", + port: process.env.ARWEAVE_PORT ? parseInt(process.env.ARWEAVE_PORT, 10) : 443, + protocol: process.env.ARWEAVE_PROTOCOL || "https", +}); + +// ⭐ Recommendation: Avoid magic numbers by defining them as constants. +const STRONG_PASSWORD_LEVEL = 3; // Corresponds to 'Strong' in check-password-strength + +// ⭐ Recommendation: Create a Set for efficient mnemonic validation (O(1) lookup). +const englishWordlistSet = new Set(wordlists.english); + + +// --- Global State --- +let globalWallet: JWKInterface | null = null; +let walletSource: 'seed_file' | 'json_file' | 'seed_phrase' | 'json_string' | null = null; + + +// --- Core Crypto Functions --- + +/** + * Generate a JWK from a mnemonic seedphrase. + * + * @param mnemonic Mnemonic seedphrase to generate wallet from. + * @returns Wallet JWK. + */ +export async function jwkFromMnemonic(mnemonic: string): Promise { + // TODO: As noted in the original code, this should be replaced. + // Use `getKeyPairFromMnemonic` from `human-crypto-keys` for a more direct and efficient implementation. + // See: https://www.notion.so/community-labs/Human-Crypto-Keys-reported-Bug-d3a8910dabb6460da814def62665181a + + const seedBuffer = await mnemonicToSeed(mnemonic); + + // Recommendation: Investigate and fix the need for @ts-ignore. + // The type of `seedBuffer` (likely Uint8Array) might differ from what `getKeyPairFromSeed` expects in Node.js (e.g., a Buffer). + // A potential fix could be `Buffer.from(seedBuffer)`. + const { privateKey } = await getKeyPairFromSeed( + //@ts-ignore + seedBuffer, + { id: "rsa", modulusLength: 4096 }, + { privateKeyFormat: "pkcs8-der" }, + ); + + // Recommendation: Investigate and fix the need for `as any`. + const jwk = await pkcs8ToJwk(privateKey as any); + + return jwk; +} + +/** + * Convert a PKCS8 private key to a JWK using Node.js's native crypto. + * + * @param privateKey PKCS8 private key to convert. + * @returns JWK. + */ +export async function pkcs8ToJwk(privateKey: Uint8Array): Promise { + const crypto = require('crypto').webcrypto; + + const key = await crypto.subtle.importKey( + "pkcs8", + privateKey, + { name: "RSA-PSS", hash: "SHA-256" }, + true, + ["sign"] + ); + + const jwk = await crypto.subtle.exportKey("jwk", key); + + // The explicit mapping is safe but could potentially be simplified + // if Arweave's JWKInterface is compatible with the standard JsonWebKey type. + return { + kty: jwk.kty!, e: jwk.e!, n: jwk.n!, + d: jwk.d, p: jwk.p, q: jwk.q, + dp: jwk.dp, dq: jwk.dq, qi: jwk.qi, + }; +} + +// --- Validation Functions --- + +/** + * Check if a password is rated as "Strong". + * + * @param password Password to check. + */ +export function checkPasswordValid(password: string): boolean { + const strength = passwordStrength(password); + return strength.id === STRONG_PASSWORD_LEVEL; +} + +/** + * Validate if a string is a valid BIP-39 mnemonic phrase using an efficient Set lookup. + * * @param mnemonic Mnemonic to validate. + * @returns `true` if the mnemonic is valid, otherwise `false`. + */ +export function isValidMnemonic(mnemonic: string): boolean { + try { + isString(mnemonic, "Mnemonic has to be a string."); + const words = mnemonic.trim().split(" "); + isOneOf(words.length, [12, 18, 24], "Invalid mnemonic length."); + + for (const word of words) { + if (!englishWordlistSet.has(word)) { + logger.warn(`Invalid word found in mnemonic: "${word}"`); + return false; + } + } + return true; + } catch (error) { + logger.error("Mnemonic validation failed", error); + return false; + } +} + + +// --- Wallet Initialization & Access --- + +/** + * ⭐ Recommendation: Refactored `initializeWallet`. + * Initializes the global wallet by trying a series of sources in a defined order of priority. + * This approach is more modular and easier to maintain. + * + * @returns JWK wallet object. + */ +export async function initializeWallet(): Promise { + if (globalWallet) { + logger.debug("[DEBUG] Wallet already initialized. Returning existing wallet."); + return globalWallet; + } + + // Define wallet sources in order of priority. + // The 'path' property for env var sources is just for logging clarity. + const sources = [ + { + type: 'seed_file', + path: process.env.SEED_FILE_PATH, + enabled: !!process.env.SEED_FILE_PATH, + load: async () => { + logger.debug(`[DEBUG] Attempting to load wallet from SEED_FILE_PATH: ${process.env.SEED_FILE_PATH}`); + const seed = await fs.readFile(process.env.SEED_FILE_PATH!, 'utf8'); + logger.debug(`[DEBUG] Read seed string from file (first 50 chars): "${seed.trim().substring(0, 50)}..."`); + if (!isValidMnemonic(seed)) throw new Error("Invalid mnemonic format in file."); + return jwkFromMnemonic(seed.trim()); + }, + }, + { + type: 'json_file', + path: process.env.WALLET_JSON_FILE_PATH, + enabled: !!process.env.WALLET_JSON_FILE_PATH, + load: async () => { + logger.debug(`[DEBUG] Attempting to load wallet from WALLET_JSON_FILE_PATH: ${process.env.WALLET_JSON_FILE_PATH}`); + const jsonString = await fs.readFile(process.env.WALLET_JSON_FILE_PATH!, 'utf8'); + logger.debug(`[DEBUG] Read JSON string from file (first 50 chars): "${jsonString.trim().substring(0, 50)}..."`); + return JSON.parse(jsonString); + }, + }, + { + type: 'seed_phrase', + path: "env var", // Placeholder for logging + enabled: !!process.env.SEED_PHRASE, + load: async () => { + logger.debug(`[DEBUG] Attempting to load wallet from SEED_PHRASE env var.`); + const seed = process.env.SEED_PHRASE!; + logger.debug(`[DEBUG] SEED_PHRASE env var content (first 50 chars): "${seed.trim().substring(0, 50)}..."`); + if (!isValidMnemonic(seed)) throw new Error("Invalid mnemonic format in env var."); + return jwkFromMnemonic(seed.trim()); + }, + }, + { + type: 'json_string', + path: "env var", // Placeholder for logging + enabled: !!process.env.WALLET_JSON, + load: async () => { + logger.debug(`[DEBUG] Attempting to load wallet from WALLET_JSON env var.`); + const jsonString = process.env.WALLET_JSON!; + logger.debug(`[DEBUG] WALLET_JSON env var content (first 50 chars): "${jsonString.trim().substring(0, 50)}..."`); + return JSON.parse(jsonString); + }, + }, + ] as const; // <--- This 'as const' is critical for type inference + + for (const source of sources) { + if (source.enabled) { + logger.debug(`[DEBUG] Checking wallet source: ${source.type.toUpperCase()}`); + try { + const wallet = await source.load(); // wallet is JWKInterface + const address = await arweave.wallets.jwkToAddress(wallet); + + logger.info(`Wallet initialized from ${source.type.toUpperCase()} with address: ${address}`); + if (source.path && source.path !== "env var") { // Log path only if it's a file path + logger.info(`Wallet source path: ${source.path}`); + } + + globalWallet = wallet; // Assign to globalWallet for future calls + walletSource = source.type; // This assignment is now type-safe due to 'as const' + + return wallet; // <--- Directly return 'wallet' which is guaranteed JWKInterface + + } catch (error) { + logger.error(`Failed to load wallet from ${source.type.toUpperCase()} (${source.path || 'no path specified'}): ${error instanceof Error ? error.message : String(error)}`); + logger.debug(`[DEBUG] Full error details for ${source.type.toUpperCase()}:`, error); + // Fall through to the next source. + } + } else { + logger.debug(`[DEBUG] Wallet source ${source.type.toUpperCase()} is not enabled or not configured.`); + } + } + + // If the loop completes without successfully returning a wallet, + // then we throw an error as no wallet could be initialized. + throw new Error("No wallet configuration could be successfully initialized from any source. Please check environment variables and file paths."); +} + +/** + * Get the initialized wallet's address. + * * @returns The wallet address string. + */ +export async function getWalletAddress(): Promise { + const wallet = await initializeWallet(); + return arweave.wallets.jwkToAddress(wallet); +} + +/** + * Get the initialized wallet for signing transactions. + * * @returns The JWK wallet object. + */ +export async function getWallet(): Promise { + return initializeWallet(); +} \ No newline at end of file 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/puzzle-generator/.env.example b/puzzle-generator/.env.example new file mode 100644 index 0000000..ee29e47 --- /dev/null +++ b/puzzle-generator/.env.example @@ -0,0 +1,7 @@ +# Database configuration +DATABASE_TYPE=sqlite # Options: "sqlite" or "postgresql" +DATABASE_NAME=localdatabase.db # For SQLite, this will be the file path; for PostgreSQL, it's the database name +DATABASE_USER= # Required for PostgreSQL, leave blank for SQLite +DATABASE_PASSWORD= # Required for PostgreSQL, leave blank for SQLite +DATABASE_HOST=localhost # Required for PostgreSQL, typically "localhost" or an IP address +DATABASE_PORT=5432 # Default PostgreSQL port, leave as-is or set if using a different port diff --git a/puzzle-generator/.gitignore b/puzzle-generator/.gitignore new file mode 100644 index 0000000..628283d --- /dev/null +++ b/puzzle-generator/.gitignore @@ -0,0 +1,8 @@ +.env +venv/ +.pytest_cache/ +__pycache__/ +.vscode/ +.coverage +htmlcov/ +*.db \ No newline at end of file diff --git a/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/puzzle-generator/conftest.py b/puzzle-generator/conftest.py new file mode 100644 index 0000000..ace4b94 --- /dev/null +++ b/puzzle-generator/conftest.py @@ -0,0 +1,5 @@ +import sys +import os + +# Add the project root directory to sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) diff --git a/puzzle-generator/docs/developing.md b/puzzle-generator/docs/developing.md new file mode 100644 index 0000000..a96e296 --- /dev/null +++ b/puzzle-generator/docs/developing.md @@ -0,0 +1,83 @@ +# Project Setup +This guide will walk you through setting up and running the Time lock puzzle project in Python. + +## Prerequisites + - Python 3.7+: Make sure you have Python installed on your system. + - GMPY2: This library is required for high-performance modular arithmetic. It provides bindings to the GMP library for Python. + +## Setting Up a Virtual Environment +It’s recommended to use a virtual environment to manage dependencies for this project. +1. Create the Virtual Environment: +```bash +python3 -m venv venv +``` +2. Activate the Virtual Environment: + - On macOS and Linux: +```bash +source venv/bin/activate +``` + - On Windows: +```bash +.\venv\Scripts\activate +``` + +## Install Dependencies: +```bash +pip install -r requirements.txt +``` +Ensure that gmpy2 is installed. If you encounter issues, you may need to install GMP and MPFR on your system (e.g., sudo apt-get install libgmp-dev libmpfr-dev on Ubuntu). +```bash +sudo apt-get install libgmp-dev libmpfr-dev +``` + +## Initializing database +```bash +python src/database/initialize_db.py +``` + + +## Running the Project +To generate a VDF proof and verify it, run the main.py script: +```bash +python main.py 10 +``` +Required Command line Arguments: + - count: the number of time lock puzzles to generate and store in the database + +## Running the Tests +To run the unit tests, use the following command: +```bash +pytest +``` +With coverage: +```bash +pytest --cov=src +``` + + + + + + +# Set version as an environment variable +export VERSION=v0.1.6 # Change this value as needed + +# Initial build and tagging for local testing +docker build -t randao/puzzle-gen:latest -t randao/puzzle-gen:$VERSION . + +# 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,linux/arm/v7 \ + -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/puzzle-generator/requirements.txt b/puzzle-generator/requirements.txt new file mode 100644 index 0000000..192aff3 --- /dev/null +++ b/puzzle-generator/requirements.txt @@ -0,0 +1,7 @@ +gmpy2 +sqlalchemy +psycopg2-binary +python-dotenv +pytest +pytest-cov +pytest-mock \ No newline at end of file diff --git a/puzzle-generator/src/__init__.py b/puzzle-generator/src/__init__.py new file mode 100644 index 0000000..e69de29 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/puzzle-generator/src/database/__init__.py b/puzzle-generator/src/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/puzzle-generator/src/database/constants.py b/puzzle-generator/src/database/constants.py new file mode 100644 index 0000000..f3822ae --- /dev/null +++ b/puzzle-generator/src/database/constants.py @@ -0,0 +1,20 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + + +DATABASE_TYPE = os.getenv("DATABASE_TYPE", "sqlite") # sqlite or postgresql +DATABASE_NAME = os.getenv("DATABASE_NAME", "localdatabase.db") +DATABASE_USER = os.getenv("DATABASE_USER", "") +DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD", "") +DATABASE_HOST = os.getenv("DATABASE_HOST", "localhost") +DATABASE_PORT = os.getenv("DATABASE_PORT", "5432") # default port for PostgreSQL + +# Create the database URL based on the database type +if DATABASE_TYPE == "sqlite": + DATABASE_URL = f"sqlite:///{DATABASE_NAME}" +else: + DATABASE_URL = ( + f"postgresql+psycopg2://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" + ) diff --git a/puzzle-generator/src/database/database.py b/puzzle-generator/src/database/database.py new file mode 100644 index 0000000..f96fd27 --- /dev/null +++ b/puzzle-generator/src/database/database.py @@ -0,0 +1,67 @@ +from sqlalchemy import create_engine, Engine +from sqlalchemy.orm import declarative_base, sessionmaker + +from src.database.constants import DATABASE_URL + +# Global variable to hold the singleton engine +_engine = None + + +def get_engine() -> Engine: + """ + Creates and returns a singleton SQLAlchemy engine connected to the database specified by DATABASE_URL. + + :return: SQLAlchemy Engine instance. + :rtype: sqlalchemy.engine.Engine + """ + global _engine + if _engine is None: + _engine = create_engine(DATABASE_URL) + return _engine + + +Base = declarative_base() # Single instance of Base + + +def get_orm_base(): + return Base + + +def save_instance(instance: any) -> None: + """ + Save an instance of an ORM model to the database. + + :param instance: The ORM model instance to save. + """ + engine = get_engine() + Session = sessionmaker(bind=engine) + session = Session() + + try: + session.add(instance) + session.commit() + except Exception as e: + session.rollback() + raise e + finally: + session.close() + + +def update_instance(instance: any) -> None: + """ + Update an instance of an ORM model in the database. + + :param instance: The ORM model instance to update. + """ + engine = get_engine() + Session = sessionmaker(bind=engine) + session = Session() + + try: + session.merge(instance) + session.commit() + except Exception as e: + session.rollback() + raise e + finally: + session.close() diff --git a/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/puzzle-generator/src/database/initialize_db.py b/puzzle-generator/src/database/initialize_db.py new file mode 100644 index 0000000..fb236c4 --- /dev/null +++ b/puzzle-generator/src/database/initialize_db.py @@ -0,0 +1,31 @@ +# src/database/initialize_db.py + +import os +import sys +from sqlalchemy.exc import OperationalError + +# Dynamically add the `src` directory to `sys.path` +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) +from src.database.entity import * + +from src.database.database import get_engine, Base + + +def initialize_database(): + """ + Initializes the SQLite database by creating all tables defined in the ORM models. + """ + engine = get_engine() + try: + print("Initializing the database...") + Base.metadata.create_all(engine) + print("Database initialized successfully.") + except OperationalError as e: + print("Failed to initialize the database:", e) + finally: + engine.dispose() # Close the engine when done + + +if __name__ == "__main__": + + initialize_database() diff --git a/puzzle-generator/src/database/mixins/__init__.py b/puzzle-generator/src/database/mixins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/puzzle-generator/src/database/mixins/saveable.py b/puzzle-generator/src/database/mixins/saveable.py new file mode 100644 index 0000000..b942ac6 --- /dev/null +++ b/puzzle-generator/src/database/mixins/saveable.py @@ -0,0 +1,9 @@ +from src.database.database import save_instance + + +class Saveable: + def save(self) -> None: + """ + Save the instance to the database. + """ + save_instance(self) diff --git a/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/puzzle-generator/src/rsa/abstract/__init__.py b/puzzle-generator/src/rsa/abstract/__init__.py new file mode 100644 index 0000000..e69de29 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..2705c61 --- /dev/null +++ b/puzzle-generator/src/time_lock_puzzle/EfficientTimeLockPuzzleSolver.py @@ -0,0 +1,47 @@ +from multiprocessing import Pool +from typing import List, Tuple + +from ..mpc import MPC +from ..utils.SystemSpecs import SystemSpecs +from ..mpc.types import MPZ +from ..rsa.RSA import RSA +from .abstract.IEfficientTimeLockPuzzleSolver import IEfficientTimeLockPuzzleSolver +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 + """ + num_workers = SystemSpecs.get_num_parallel_processes() + with Pool(num_workers) 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..1e05a0e --- /dev/null +++ b/puzzle-generator/src/time_lock_puzzle/TimeLockPuzzleFactory.py @@ -0,0 +1,81 @@ +from typing import List, Tuple +import multiprocessing + +from src.time_lock_puzzle import TimeLockPuzzleBuilder +from ..utils.SystemSpecs import SystemSpecs +from ..mpc import MPC +from ..mpc.types import MPZ +from ..random import Random +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)] + + num_workers = SystemSpecs.get_num_parallel_processes() + + # Create puzzles in parallel using process pool + with multiprocessing.Pool(num_workers) 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/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py b/puzzle-generator/src/time_lock_puzzle/abstract/__init__.py new file mode 100644 index 0000000..e69de29 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/puzzle-generator/src/utils/SystemSpecs.py b/puzzle-generator/src/utils/SystemSpecs.py new file mode 100644 index 0000000..8c4dbe4 --- /dev/null +++ b/puzzle-generator/src/utils/SystemSpecs.py @@ -0,0 +1,20 @@ +"""Utility class for system specifications and resource management.""" + +import multiprocessing + + +class SystemSpecs: + """Utility class for determining system specifications and resource allocation.""" + + @staticmethod + def get_num_parallel_processes() -> int: + """ + Calculate the optimal number of parallel processes to use. + + Returns half the number of CPU cores, with a minimum of 1. + + Returns: + int: Number of parallel processes to use + """ + parallelization_denominator = 2 # if cpu has 16 cores and parallelization denominator is 2 then this codebase will use 8 cores + return multiprocessing.cpu_count() // parallelization_denominator or 1 # default to 1 if only 1 core available diff --git a/puzzle-generator/src/utils/__init__.py b/puzzle-generator/src/utils/__init__.py new file mode 100644 index 0000000..d3d10ee --- /dev/null +++ b/puzzle-generator/src/utils/__init__.py @@ -0,0 +1,5 @@ +"""Utility modules for the puzzle generator.""" + +from .SystemSpecs import SystemSpecs + +__all__ = ["SystemSpecs"] 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..49550a1 --- /dev/null +++ b/requester/package.json @@ -0,0 +1,28 @@ +{ + "devDependencies": { + "@types/dockerode": "^3.3.31", + "@types/node": "^22.9.1", + "@types/pg": "^8.11.10", + "typescript": "^5.6.3" + }, + "dependencies": { + "@permaweb/aoconnect": "^0.0.78", + "ao-process-clients": "^6.0.67", + "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..5b4f890 --- /dev/null +++ b/requester/src/app.ts @@ -0,0 +1,212 @@ +import { + RandomClient, +} from "ao-process-clients"; +//import { TransferToProviders } from "./extra"; + +const RETRY_DELAY_MS = 5000; // 1 seconds +const PROVIDER_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes +const PROVIDER_REQUEST_TIMEOUT = 60 * 1000; // 1 minute +const CHANCE_TO_CALL_RANDOM = 1; + +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", +// MODE: "legacy" +// }; + +let randomClientInstance: RandomClient | null = null; + +export async function getRandomClient(): Promise { + + if (!randomClientInstance) { + randomClientInstance = ((await RandomClient.defaultBuilder())) + .withAOConfig({ + MU_URL: "https://ur-mu.randao.net", + CU_URL: "https://ur-cu.randao.net", + // MU_URL: "https://mu.ao-testnet.xyz", + // CU_URL: "https://cu.ao-testnet.xyz", + GATEWAY_URL: "https://arweave.net", + MODE: "legacy" + }) + .withWallet(JSON.parse(process.env.REQUEST_WALLET_JSON!)) + .build(); + } + 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 + } +} + +import { startRequestTracker } from "./requestTracker"; + +async function main() { + const randclient = await getRandomClient() + //const stakeclient = ProviderStakingClient.autoConfiguration(); + + randclient.prepay(1000_000000000) //1,000 + + // Start request tracker in a separate process + // This will continuously poll for provider activity and crank defunct requests + startRequestTracker().catch(error => { + console.error("Error starting request tracker:", error); + }); + while (true) { + console.log("Running") + try { + // 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); + console.log(await randclient.redeem(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/requestTracker.ts b/requester/src/requestTracker.ts new file mode 100644 index 0000000..b0dae03 --- /dev/null +++ b/requester/src/requestTracker.ts @@ -0,0 +1,171 @@ +import { RandomClient } from "ao-process-clients"; +import { getRandomClient } from "./app"; + +// Map to track request timestamps +const requestTimestamps: Map = new Map(); +const DEFUNCT_THRESHOLD_MS = 30 * 1000; // 30 seconds +const POLL_INTERVAL_MS = 1000; // Poll every second + +/** + * Function to log request timestamps + * Adds new request IDs to the tracking map and removes ones that are no longer present + * @param allRequestIds Array of request IDs to track + */ +function logRequestTimestamps(allRequestIds: string[]): void { + const currentTime = Date.now(); + const existingIds = new Set(requestTimestamps.keys()); + + // Add new request IDs with current timestamp + for (const requestId of allRequestIds) { + if (!requestTimestamps.has(requestId)) { + console.log(`Adding new request ID to tracking: ${requestId}`); + requestTimestamps.set(requestId, currentTime); + } + } + + // Remove request IDs that are no longer present + for (const existingId of existingIds) { + if (!allRequestIds.includes(existingId)) { + console.log(`Removing request ID from tracking: ${existingId}`); + requestTimestamps.delete(existingId); + } + } +} + +/** + * Check for defunct requests and crank if needed + */ +async function crankDefunctRequests(randclient: RandomClient) { + const currentTime = Date.now(); + const defunctRequestIds: string[] = []; + + // Check for defunct request IDs (those that have been in the map for over 30 seconds) + requestTimestamps.forEach((timestamp, requestId) => { + const timeInMap = currentTime - timestamp; + if (timeInMap > DEFUNCT_THRESHOLD_MS) { + defunctRequestIds.push(requestId); + console.log(`Defunct request found: ${requestId} (in system for ${Math.floor(timeInMap / 1000)} seconds)`); + } + }); + + // If there are any defunct requests, run the crank + if (defunctRequestIds.length > 0) { + console.log(`Cranking due to ${defunctRequestIds.length} defunct requests: ${defunctRequestIds.join(', ')}`); + await randclient.crank(); + } +} + +/** + * Parse provider activity to extract all request IDs + * @param providerActivity Provider activity data from getAllProviderActivity + * @returns Array of request IDs + */ +function extractRequestIdsFromProviderActivity(providerActivity: any[]): string[] { + const allRequestIds: string[] = []; + + // Process each provider to extract request IDs + for (const provider of providerActivity) { + try { + // Extract challenge request IDs + if (provider.active_challenge_requests && typeof provider.active_challenge_requests === 'string') { + try { + const parsedChallengeData = JSON.parse(provider.active_challenge_requests); + if (parsedChallengeData && typeof parsedChallengeData === 'object' && 'request_ids' in parsedChallengeData) { + const requestIds = parsedChallengeData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + console.warn(`Warning: Failed to parse challenge requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + + // Extract output request IDs + if (provider.active_output_requests && typeof provider.active_output_requests === 'string') { + try { + const parsedOutputData = JSON.parse(provider.active_output_requests); + if (parsedOutputData && typeof parsedOutputData === 'object' && 'request_ids' in parsedOutputData) { + const requestIds = parsedOutputData.request_ids; + if (Array.isArray(requestIds)) { + for (const id of requestIds) { + if (typeof id === 'string') { + allRequestIds.push(id); + } + } + } + } + } catch (parseErr) { + console.warn(`Warning: Failed to parse output requests JSON for provider ${provider.provider_id}:`, parseErr); + } + } + } catch (err) { + console.warn(`Warning: Failed to process provider ${provider?.provider_id || 'unknown'}:`, err); + } + } + + // Remove duplicates + return [...new Set(allRequestIds)]; +} + +/** + * Main function to start tracking and cranking requests + */ +export async function startRequestTracker() { + console.log("Starting request tracker..."); + + while (true) { + try { + const randclient = await getRandomClient(); + let maxRetries = 3; + let response = null; + let lastError = null; + + // Get provider activity with retries + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + response = await randclient.getAllProviderActivity(); + lastError = null; + break; // Success, exit retry loop + } catch (error) { + lastError = error as Error; + console.warn(`Attempt ${attempt}/${maxRetries} failed to fetch provider activity:`, error); + + if (attempt < maxRetries) { + // Wait before retrying with exponential backoff + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + // If we still have an error after retries, throw it + if (lastError) { + throw lastError; + } + + if (!response) { + throw new Error('No response from provider activity'); + } + + // Extract all request IDs + const allRequestIds = extractRequestIdsFromProviderActivity(response); + console.log(`Found ${allRequestIds.length} active request IDs across all providers`); + + // Log timestamps for tracking + logRequestTimestamps(allRequestIds); + + // Check and crank defunct requests + await crankDefunctRequests(randclient); + + } catch (error) { + console.error("Error in request tracker:", error); + } + + // Wait before next polling cycle + await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)); + } +} 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/todd-updates.md b/todd-updates.md new file mode 100644 index 0000000..1722644 --- /dev/null +++ b/todd-updates.md @@ -0,0 +1,252 @@ +# Randao Provider Configuration Guide + +This document outlines how to deploy the Randao Provider application using Docker Compose, covering configurations for both standard user environments (e.g., Windows, macOS, Linux desktop) and dedicated Linux service/appliance environments. + +----- + +## 1\. Core Concepts & Files + +The Randao Provider deployment relies on these key files: + + * **`docker-compose.yml`**: The **base** Docker Compose file. It defines the core services (orchestrator, PostgreSQL), their dependencies, and common environment variables. It uses relative paths for `wallet.json` and `postgresql.conf` for portability and serves as the default configuration for standard user environments. + * **`.env`**: A plain text file storing environment variables (database credentials, network settings). This file is sourced by Docker Compose. + * **`wallet.json`**: Your Arweave wallet's JWK (JSON Web Key) file. This contains sensitive private key information. + * **`wallet.seed`** (Optional): If you use a mnemonic seed phrase instead of a JWK. + * **`docker-compose.appliance.yml`**: An **override** Docker Compose file specifically for appliance deployments. It defines absolute paths for sensitive files (like `wallet.json`) and appliance-specific resource limits. + * **`postgres/postgresql.conf`**: Custom PostgreSQL configuration for resource-constrained environments. + * **Wallet Management**: The application's source code (specifically `walletUtils.ts`) has been modified to prioritize reading wallet information securely from mounted files (e.g., `wallet.json` or `wallet.seed`), falling back to environment variables (`WALLET_JSON` or `SEED_PHRASE`) if files aren't found. This guide assumes you are using an image that includes these modifications. + +----- + +## 2\. Why Use Wallet Files Instead of Environment Variables? + +Using dedicated files (like `wallet.json` or `wallet.seed`) for sensitive wallet information is **strongly preferred for security reasons** over passing this data directly as environment variables (`WALLET_JSON` or `SEED_PHRASE`). + +Here's why: + + * **Reduced Visibility (Primary Reason):** + * **Environment Variables (`WALLET_JSON`, `SEED_PHRASE`):** These are notoriously insecure for sensitive data. Anyone with access to the Docker host (even a non-root user with `docker` group access) can easily inspect a running container's environment variables using the `docker inspect ` command. This means your full wallet private key could be displayed in plain text in the command's output. + * **Files (`wallet.json`, `wallet.seed`):** When you mount a file into a container (e.g., `/etc/randao/wallet.json` into `/app/config/wallet.json`), the file's content is not directly exposed as an environment variable of the running process. An attacker would need filesystem access to `/etc/randao/wallet.json` on the host (which can be protected with strict permissions), *and* potentially shell access *inside* the container, to read the file. + * **Principle of Least Privilege (Filesystem):** You can set very tight file permissions on the host (e.g., `chmod 640` or `600`) for `wallet.json` and `wallet.seed`. This allows only the necessary user (e.g., `root` for ownership, `randao_service` user for read access via group) to access the file, further limiting exposure. + * **Best Practice:** Mounting sensitive data as files is the industry-standard best practice for secret management in containerized environments (e.g., Docker Secrets in Swarm mode, Kubernetes Secrets mounted as volumes). + * **Logging:** Environment variables can sometimes inadvertently end up in logs if the application or logging system isn't carefully configured. File contents are less prone to this leakage. + +While the application supports falling back to environment variables for convenience, **using the file-based method for your wallet is always the more secure choice, especially for production or appliance deployments.** + +----- + +## 3\. Setting Up the Project Directory + +Begin by cloning the Randao Provider repository from GitHub and organizing your configuration files. + +### **3.1. Clone the Repository:** + +```bash +git clone https://github.com/RandAOLabs/Randomness-Provider.git your-randao-provider-repo +``` + +### **3.2. Navigate to the Docker Compose Directory:** + +```bash +cd your-randao-provider-repo/docker-compose/ +``` + +### **3.3. Project Directory Structure:** + +Your directory should look similar to this: + +``` +your-randao-provider-repo/ +├── docker-compose/ +│ ├── docker-compose.yml # Base Docker Compose file +│ ├── docker-compose.appliance.yml # Appliance-specific overrides +│ ├── postgres/ +│ │ └── postgresql.conf # Custom Postgres config +│ ├── .env.example # Example .env file (for users to copy) +│ ├── wallet.json.example # Example wallet.json (for users to copy) +│ └── wallet.seed.example # Example wallet.seed (optional) +├── orchestrator/ # Contains Dockerfile and walletUtils.ts (source, not used directly by docker compose up) +│ └── Dockerfile +│ └── src/walletUtils.ts +├── LICENSE +└── README.md +``` + +----- + +## 4\. Configuration for a Standard Docker User (e.g., Windows, macOS, Linux Desktop) + +This setup is for users who want to run the provider locally without systemd integration, using pre-built Docker images. + +### **4.1. Prerequisites:** + + * **Docker Desktop** (Windows/macOS) or **Docker Engine** (Linux) installed and running. + * Access to the command line/terminal. + +### **4.2. Setup Steps:** + +1. **Navigate to the `docker-compose` directory** (if not already there): + + ```bash + cd your-randao-provider-repo/docker-compose/ + ``` + +2. **Create `.env` file:** + Copy the example `.env` file and **fill in your database credentials**. + + ```bash + cp .env.example .env + # Open .env in a text editor and fill in DB_USER, DB_PASSWORD, DB_NAME, DOCKER_NETWORK, LOG_CONSOLE_LEVEL + # Example .env content: + # DB_USER=myuser + # DB_PASSWORD=mypassword + # DB_NAME=mydatabase + # DOCKER_NETWORK=backend + # LOG_CONSOLE_LEVEL=7 + ``` + +3. **Create `wallet.json` (or `wallet.seed`):** + Copy the example wallet file and **paste your actual Arweave wallet's JWK content** (or seed phrase) into it. + + ```bash + cp wallet.json.example wallet.json + # Open wallet.json in a text editor and paste your JWK content. + # On Linux/macOS, set permissions for security: + chmod 600 wallet.json + ``` + + * **Fallback Option:** If you prefer not to create `wallet.json` directly, you can put `WALLET_JSON='{"your_jwk_content"}'` directly into your `.env` file. The application code will fall back to this environment variable if it cannot read `wallet.json` from the mounted file. **Note that this fallback is less secure.** + +4. **Run the Docker Compose Stack:** + Use the base `docker-compose.yml`. `docker compose` will pull the necessary images. + + ```bash + docker compose up --pull=always + ``` + + * `--pull=always`: Ensures the latest image versions are pulled from Docker Hub. + * `up`: Starts the services in the foreground. Add `-d` to run in detached mode (background). + +5. **Monitor Logs:** + + ```bash + docker compose logs -f + ``` + +----- + +## 5\. Configuration for a Linux Service / Appliance + +This setup provides robust, automated management via `systemd`, enhanced security, and consistent updates, using pre-built Docker images. + +### **5.1. Prerequisites:** + + * **Debian/Ubuntu** (or similar Linux distribution) installed. + * **Docker Engine** and **Docker Compose V2** installed. + * **`randao_service` system user** created (e.g., `sudo adduser --system --no-create-home --group --uid 888 randao_service`). + * `randao_service` user added to the `docker` group (e.g., `sudo usermod -aG docker randao_service`). + * **Swap space** configured (highly recommended for low-RAM devices like H3). + * **Ownership and permissions** for the project directory set for `randao_service`. + ```bash + sudo chown -R randao_service:randao_service /home/randao/Randomness-Provider.git/ + sudo chmod -R u=rwX,go=rX /home/randao/Randomness-Provider.git/ + ``` + +### **5.2. Setup Steps:** + +1. **Place Sensitive Configuration Files in `/etc/randao/`:** + These files are managed by `root` but readable by `randao_service`. This is the **preferred and most secure location** for appliance secrets. + + ```bash + # Create the directory + sudo mkdir -p /etc/randao/ + + # Copy your actual .env and wallet.json files from your local setup or provisioning source + # Example (assuming they are temporarily available in /tmp/ during provisioning): + sudo cp /tmp/.env /etc/randao/.env + sudo cp /tmp/wallet.json /etc/randao/wallet.json + sudo cp /tmp/wallet.seed /etc/randao/wallet.seed # If using seed file + + # Set ownership and permissions for the directory + sudo chown root:root /etc/randao/ + sudo chmod 700 /etc/randao/ # Root only access to the directory itself + + # Set ownership and permissions for the files + sudo chown root:randao_service /etc/randao/.env + sudo chmod 640 /etc/randao/.env # Root R/W, randao_service group R, others no access + + sudo chown root:randao_service /etc/randao/wallet.json + sudo chmod 640 /etc/randao/wallet.json + + # If wallet.seed is used + sudo chown root:randao_service /etc/randao/wallet.seed + sudo chmod 640 /etc/randao/wallet.seed + ``` + +2. **Place `docker-compose` Project Files:** + Copy the cloned repository contents to a system location like `/home/randao/Randomness-Provider.git/`. + + ```bash + # Example: + sudo cp -r /path/to/your/cloned-repo/Randomness-Provider.git /home/randao/ + ``` + +3. **Create Systemd Service Unit (`randao.service`):** + Create `/etc/systemd/system/randao.service` with the following content: + + ```ini + # /etc/systemd/system/randao.service + [Unit] + Description=RANDAO Provider + Documentation=https://github.com/RandAOLabs/Randomness-Provider + Requires=docker.service + After=network-online.target docker.service + + [Service] + Type=simple + User=randao_service + Group=randao_service + WorkingDirectory=/home/randao/Randomness-Provider.git/docker-compose + ExecStart=/usr/bin/docker compose -f docker-compose.yml -f docker-compose.appliance.yml --env-file /etc/randao/.env up --pull=always + ExecStop=/usr/bin/docker compose down + TimeoutStartSec=0 + Restart=on-failure + RestartSec=5s + + [Install] + WantedBy=multi-user.target + ``` + +4. **Create Systemd Timer Unit (`randao.timer`):** + Create `/etc/systemd/system/randao.timer` with the following content for periodic updates: + + ```ini + # /etc/systemd/system/randao.timer + [Unit] + Description=Timer to periodically restart RANDAO Provider for latest image pull + + [Timer] + OnCalendar=*-*-* 00,12:00:00 # Restart every day at midnight and noon UTC + RandomizedDelaySec=30min # Add a random delay to prevent stampedes + Persistent=true # Trigger on boot if a scheduled run was missed + OnBootSec=10s # Start 10 seconds after system boot (initial run) + + [Install] + WantedBy=timers.target + ``` + +5. **Enable & Start Services:** + + ```bash + sudo systemctl daemon-reload # Reload systemd to recognize new units + sudo systemctl enable randao.timer # Enable the timer for autostart on reboot + sudo systemctl start randao.timer # Start the timer immediately + # The timer will then trigger randao.service (e.g., after 10 seconds due to OnBootSec) + ``` + +6. **Monitor Logs:** + + ```bash + sudo journalctl -u randao.service -f # Monitor real-time logs from your service + sudo systemctl status randao.timer # Check timer's status and next activation + ``` \ No newline at end of file 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