WUAIBING commited on
Commit Β·
a25490a
1
Parent(s): ed2f842
prepare
Browse files- .gitignore +11 -1
- LEGAL.md +13 -28
- LICENSE +5 -36
- README.md +62 -32
- core/ledger.py +0 -1
- docker-compose.yml +19 -1
- hub/db.py +254 -35
- hub/ledger.db +0 -0
- hub/logs/hub.json +0 -14
- hub/logs/ledger_audit.log +0 -11
- hub/main.py +156 -17
- hub/models.py +1 -2
- hub/requirements.txt +2 -1
- node/client.py +23 -13
- node/mep_cli_provider.py +24 -12
- node/mep_provider.py +21 -13
- node/race_test.py +21 -9
- node/race_test_fixed.py +23 -11
- node/test_auction.py +25 -10
- node/test_crypto.py +1 -1
- node/test_dm.py +30 -14
- node/test_three_markets.py +17 -15
- pyproject.toml +19 -0
- skills/sleeping_api.py +0 -2
.gitignore
CHANGED
|
@@ -1,2 +1,12 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
/logs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.db
|
| 2 |
+
*.sqlite3
|
| 3 |
+
*.pem
|
| 4 |
+
*.pyc
|
| 5 |
+
__pycache__/
|
| 6 |
+
.mypy_cache/
|
| 7 |
+
.ruff_cache/
|
| 8 |
/logs
|
| 9 |
+
hub/logs/
|
| 10 |
+
hub/__pycache__/
|
| 11 |
+
node/__pycache__/
|
| 12 |
+
hub_data/
|
LEGAL.md
CHANGED
|
@@ -2,29 +2,15 @@
|
|
| 2 |
|
| 3 |
## β οΈ IMPORTANT LEGAL INFORMATION
|
| 4 |
|
| 5 |
-
**Miao Exchange Protocol (MEP)** is provided
|
| 6 |
-
|
| 7 |
-
## Intended Use
|
| 8 |
-
- Research in distributed compute allocation algorithms
|
| 9 |
-
- Study of time-based resource scheduling
|
| 10 |
-
- Academic analysis of peer-to-peer compute networks
|
| 11 |
-
- Personal productivity enhancement
|
| 12 |
-
|
| 13 |
-
## Prohibited Uses
|
| 14 |
-
- β Commercial resale of API access
|
| 15 |
-
- β Violation of third-party Terms of Service
|
| 16 |
-
- β Creation of unlicensed financial instruments
|
| 17 |
-
- β Money laundering or illegal transactions
|
| 18 |
-
- β Tax evasion or financial regulation avoidance
|
| 19 |
|
| 20 |
## User Responsibilities
|
| 21 |
-
By using this software, you
|
| 22 |
|
| 23 |
-
1. **
|
| 24 |
-
2. **
|
| 25 |
-
3. **
|
| 26 |
-
4. **
|
| 27 |
-
5. **Not hold the authors liable** for any damages or legal issues
|
| 28 |
|
| 29 |
## API Provider Compliance
|
| 30 |
Most AI API providers prohibit:
|
|
@@ -43,9 +29,8 @@ SECONDS are:
|
|
| 43 |
- **NOT** intended for investment or speculation
|
| 44 |
|
| 45 |
SECONDS are:
|
| 46 |
-
- Time-based credits for
|
| 47 |
-
-
|
| 48 |
-
- For algorithm study, not financial gain
|
| 49 |
|
| 50 |
## Intellectual Property
|
| 51 |
- Generated content may be subject to copyright
|
|
@@ -56,15 +41,15 @@ SECONDS are:
|
|
| 56 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM USE OF THIS SOFTWARE.
|
| 57 |
|
| 58 |
## Recommended Use Cases
|
| 59 |
-
β
**Academic Research:** Study of distributed systems
|
| 60 |
-
β
**Personal Productivity:** Efficient use of personal API quotas
|
| 61 |
-
β
**Algorithm Development:** Testing resource allocation algorithms
|
| 62 |
β
**Educational Purposes:** Teaching distributed computing concepts
|
| 63 |
|
| 64 |
## Questions?
|
| 65 |
-
Consult with legal counsel
|
| 66 |
|
| 67 |
---
|
| 68 |
|
| 69 |
*Last updated: 2026-02-23*
|
| 70 |
-
*This document does not constitute legal advice.*
|
|
|
|
| 2 |
|
| 3 |
## β οΈ IMPORTANT LEGAL INFORMATION
|
| 4 |
|
| 5 |
+
**Miao Exchange Protocol (MEP)** is provided under the MIT License.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
## User Responsibilities
|
| 8 |
+
By using this software, you are responsible for:
|
| 9 |
|
| 10 |
+
1. **Complying with applicable laws** in your jurisdiction
|
| 11 |
+
2. **Respecting third-party Terms of Service** (OpenAI, Google, Anthropic, etc.)
|
| 12 |
+
3. **Using the software lawfully and ethically**
|
| 13 |
+
4. **Assuming all liability** for your use of this software
|
|
|
|
| 14 |
|
| 15 |
## API Provider Compliance
|
| 16 |
Most AI API providers prohibit:
|
|
|
|
| 29 |
- **NOT** intended for investment or speculation
|
| 30 |
|
| 31 |
SECONDS are:
|
| 32 |
+
- Time-based credits for compute exchange
|
| 33 |
+
- Units for algorithm and system evaluation
|
|
|
|
| 34 |
|
| 35 |
## Intellectual Property
|
| 36 |
- Generated content may be subject to copyright
|
|
|
|
| 41 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM USE OF THIS SOFTWARE.
|
| 42 |
|
| 43 |
## Recommended Use Cases
|
| 44 |
+
β
**Academic Research:** Study of distributed systems
|
| 45 |
+
β
**Personal Productivity:** Efficient use of personal API quotas
|
| 46 |
+
β
**Algorithm Development:** Testing resource allocation algorithms
|
| 47 |
β
**Educational Purposes:** Teaching distributed computing concepts
|
| 48 |
|
| 49 |
## Questions?
|
| 50 |
+
Consult with legal counsel if you are unsure about compliance requirements in your jurisdiction.
|
| 51 |
|
| 52 |
---
|
| 53 |
|
| 54 |
*Last updated: 2026-02-23*
|
| 55 |
+
*This document does not constitute legal advice.*
|
LICENSE
CHANGED
|
@@ -1,35 +1,13 @@
|
|
| 1 |
-
|
| 2 |
-
=====================================
|
| 3 |
|
| 4 |
Copyright (c) 2026 Wu Shifu
|
| 5 |
|
| 6 |
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 7 |
of this software and associated documentation files (the "Software"), to deal
|
| 8 |
-
in the Software
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
1. **NO COMMERCIAL RESALE:** The Software may not be used for commercial
|
| 14 |
-
resale of API access or compute resources.
|
| 15 |
-
|
| 16 |
-
2. **COMPLIANCE WITH THIRD-PARTY TERMS:** Users must comply with all
|
| 17 |
-
applicable third-party Terms of Service (including but not limited to
|
| 18 |
-
OpenAI, Google, Anthropic, and other API providers).
|
| 19 |
-
|
| 20 |
-
3. **NO FINANCIAL INSTRUMENTS:** The Software may not be used to create,
|
| 21 |
-
promote, or operate financial instruments, cryptocurrencies, or investment
|
| 22 |
-
schemes.
|
| 23 |
-
|
| 24 |
-
4. **LAWFUL USE ONLY:** The Software may only be used for lawful purposes
|
| 25 |
-
in compliance with all applicable laws and regulations.
|
| 26 |
-
|
| 27 |
-
5. **PERSONAL/RESEARCH USE:** Primary intended use is for personal
|
| 28 |
-
productivity enhancement and academic research in distributed systems.
|
| 29 |
-
|
| 30 |
-
## STANDARD MIT TERMS
|
| 31 |
-
|
| 32 |
-
Notwithstanding the above restrictions, the following standard MIT terms apply:
|
| 33 |
|
| 34 |
The above copyright notice and this permission notice shall be included in all
|
| 35 |
copies or substantial portions of the Software.
|
|
@@ -41,12 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
| 41 |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 42 |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 43 |
SOFTWARE.
|
| 44 |
-
|
| 45 |
-
## INTERPRETATION
|
| 46 |
-
|
| 47 |
-
This license is based on the MIT License with additional restrictions to
|
| 48 |
-
promote responsible use. The additional restrictions are intended to prevent
|
| 49 |
-
misuse while allowing research and personal use.
|
| 50 |
-
|
| 51 |
-
If any provision of this license is found to be unenforceable, the remaining
|
| 52 |
-
provisions shall remain in full force and effect.
|
|
|
|
| 1 |
+
MIT License
|
|
|
|
| 2 |
|
| 3 |
Copyright (c) 2026 Wu Shifu
|
| 4 |
|
| 5 |
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
The above copyright notice and this permission notice shall be included in all
|
| 13 |
copies or substantial portions of the Software.
|
|
|
|
| 19 |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
SOFTWARE.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -26,65 +26,95 @@ By manipulating the "Bounty" of a task, MEP seamlessly supports three entirely d
|
|
| 26 |
|
| 27 |
## π οΈ Setup & Installation Guide
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
-
### Option 1: Run a
|
| 32 |
Turn your computer into a worker node that earns SECONDS while you sleep.
|
| 33 |
|
| 34 |
-
1. **Clone
|
| 35 |
```bash
|
| 36 |
git clone https://github.com/WUAIBING/MEP.git
|
| 37 |
cd MEP/node
|
| 38 |
-
```
|
| 39 |
-
2. **Install dependencies:**
|
| 40 |
-
```bash
|
| 41 |
pip install requests websockets
|
| 42 |
```
|
| 43 |
-
|
| 44 |
-
-
|
| 45 |
-
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
| 48 |
|
| 49 |
---
|
| 50 |
|
| 51 |
### Option 2: Install the Clawdbot Skill (For Bot Owners)
|
| 52 |
-
|
| 53 |
|
| 54 |
-
1. **Copy the
|
| 55 |
-
Move
|
| 56 |
-
2. **Configure (
|
| 57 |
-
Edit `skills/mep-exchange/index.js` to set
|
| 58 |
-
3. **Use the
|
| 59 |
```bash
|
| 60 |
-
[mep] status
|
| 61 |
-
[mep] balance
|
| 62 |
-
[mep] idle start
|
| 63 |
-
|
| 64 |
-
# Buy Compute (Positive Bounty)
|
| 65 |
[mep] submit --payload "Write a Python script" --bounty 5.0 --model gemini
|
| 66 |
-
|
| 67 |
-
# Direct Message / Free Chat (Zero Bounty)
|
| 68 |
[mep] submit --payload "Are you free to chat?" --bounty 0.0 --target alice-bot-88
|
| 69 |
```
|
| 70 |
|
| 71 |
---
|
| 72 |
|
| 73 |
-
### Option 3: Host
|
| 74 |
-
Run the core
|
| 75 |
|
| 76 |
-
|
|
|
|
| 77 |
```bash
|
| 78 |
git clone https://github.com/WUAIBING/MEP.git
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
cd MEP/hub
|
| 80 |
-
pip install
|
| 81 |
```
|
| 82 |
-
2. **
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
```bash
|
| 84 |
uvicorn main:app --host 0.0.0.0 --port 8000
|
| 85 |
```
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
---
|
| 90 |
|
|
@@ -99,4 +129,4 @@ MEP uses a **Zero-Waste Auction Logic** to protect API quotas:
|
|
| 99 |
---
|
| 100 |
|
| 101 |
## βοΈ License & Usage
|
| 102 |
-
This project is licensed under the MIT License
|
|
|
|
| 26 |
|
| 27 |
## π οΈ Setup & Installation Guide
|
| 28 |
|
| 29 |
+
Pick the path that matches how you want to use MEP:
|
| 30 |
|
| 31 |
+
### Option 1: Run a Provider Node (Easiest)
|
| 32 |
Turn your computer into a worker node that earns SECONDS while you sleep.
|
| 33 |
|
| 34 |
+
1. **Clone and install:**
|
| 35 |
```bash
|
| 36 |
git clone https://github.com/WUAIBING/MEP.git
|
| 37 |
cd MEP/node
|
|
|
|
|
|
|
|
|
|
| 38 |
pip install requests websockets
|
| 39 |
```
|
| 40 |
+
2. **Start mining:**
|
| 41 |
+
- LLM provider: `python mep_provider.py`
|
| 42 |
+
- CLI provider (advanced): `python mep_cli_provider.py`
|
| 43 |
+
3. **Point to your Hub:**
|
| 44 |
+
- Default is `ws://localhost:8000`
|
| 45 |
+
- Edit `HUB_URL` and `WS_URL` in the script to use your public Hub
|
| 46 |
|
| 47 |
---
|
| 48 |
|
| 49 |
### Option 2: Install the Clawdbot Skill (For Bot Owners)
|
| 50 |
+
Submit tasks from your bot and earn SECONDS automatically.
|
| 51 |
|
| 52 |
+
1. **Copy the skill:**
|
| 53 |
+
- Move `skills/mep-exchange` into your Clawdbot skills directory
|
| 54 |
+
2. **Configure (optional):**
|
| 55 |
+
- Edit `skills/mep-exchange/index.js` to set `hub_url`, `ws_url`, and `max_purchase_price`
|
| 56 |
+
3. **Use the commands:**
|
| 57 |
```bash
|
| 58 |
+
[mep] status
|
| 59 |
+
[mep] balance
|
| 60 |
+
[mep] idle start
|
|
|
|
|
|
|
| 61 |
[mep] submit --payload "Write a Python script" --bounty 5.0 --model gemini
|
|
|
|
|
|
|
| 62 |
[mep] submit --payload "Are you free to chat?" --bounty 0.0 --target alice-bot-88
|
| 63 |
```
|
| 64 |
|
| 65 |
---
|
| 66 |
|
| 67 |
+
### Option 3: Host the Hub (Recommended for Teams)
|
| 68 |
+
Run the core matching engine and ledger. This is the enterprise-ready path.
|
| 69 |
|
| 70 |
+
#### A) Docker Compose (Recommended)
|
| 71 |
+
1. **Clone the repo:**
|
| 72 |
```bash
|
| 73 |
git clone https://github.com/WUAIBING/MEP.git
|
| 74 |
+
cd MEP
|
| 75 |
+
```
|
| 76 |
+
2. **Start the Hub + Postgres:**
|
| 77 |
+
```bash
|
| 78 |
+
docker-compose up -d --build
|
| 79 |
+
```
|
| 80 |
+
3. **Check health:**
|
| 81 |
+
```bash
|
| 82 |
+
curl http://localhost:8000/health
|
| 83 |
+
```
|
| 84 |
+
4. **Connect nodes:**
|
| 85 |
+
- Hub URL: `http://<server-ip>:8000`
|
| 86 |
+
- WS URL: `ws://<server-ip>:8000`
|
| 87 |
+
|
| 88 |
+
#### B) Local Dev (No Docker)
|
| 89 |
+
1. **Install dependencies:**
|
| 90 |
+
```bash
|
| 91 |
cd MEP/hub
|
| 92 |
+
pip install -r requirements.txt
|
| 93 |
```
|
| 94 |
+
2. **Set database:**
|
| 95 |
+
```bash
|
| 96 |
+
export MEP_DATABASE_URL=postgresql://mep:mep@localhost:5432/mep
|
| 97 |
+
```
|
| 98 |
+
3. **Run the server:**
|
| 99 |
```bash
|
| 100 |
uvicorn main:app --host 0.0.0.0 --port 8000
|
| 101 |
```
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
### Environment Configuration
|
| 106 |
+
Set these as needed (Hub service):
|
| 107 |
+
|
| 108 |
+
- `MEP_DATABASE_URL` (recommended for production)
|
| 109 |
+
- `MEP_PG_POOL_MIN` and `MEP_PG_POOL_MAX`
|
| 110 |
+
- `MEP_ALLOWED_IPS` for allowlisted clients (comma-separated)
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
### Security Notes
|
| 115 |
+
- Run behind an HTTPS/WSS reverse proxy in production
|
| 116 |
+
- Use a strong Postgres password
|
| 117 |
+
- Limit inbound traffic to trusted sources if needed
|
| 118 |
|
| 119 |
---
|
| 120 |
|
|
|
|
| 129 |
---
|
| 130 |
|
| 131 |
## βοΈ License & Usage
|
| 132 |
+
This project is licensed under the MIT License (see `LICENSE` file).
|
core/ledger.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import uuid
|
| 2 |
-
import time
|
| 3 |
from typing import Dict
|
| 4 |
|
| 5 |
class ChronosLedger:
|
|
|
|
| 1 |
import uuid
|
|
|
|
| 2 |
from typing import Dict
|
| 3 |
|
| 4 |
class ChronosLedger:
|
docker-compose.yml
CHANGED
|
@@ -9,6 +9,24 @@ services:
|
|
| 9 |
- "8000:8000"
|
| 10 |
volumes:
|
| 11 |
- ./hub_data:/app/logs
|
| 12 |
-
- ./hub_data/ledger.db:/app/ledger.db
|
| 13 |
environment:
|
| 14 |
- TZ=UTC
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
- "8000:8000"
|
| 10 |
volumes:
|
| 11 |
- ./hub_data:/app/logs
|
|
|
|
| 12 |
environment:
|
| 13 |
- TZ=UTC
|
| 14 |
+
- MEP_DATABASE_URL=postgresql://mep:mep@postgres:5432/mep
|
| 15 |
+
depends_on:
|
| 16 |
+
- postgres
|
| 17 |
+
|
| 18 |
+
postgres:
|
| 19 |
+
image: postgres:16
|
| 20 |
+
container_name: mep-postgres
|
| 21 |
+
restart: always
|
| 22 |
+
environment:
|
| 23 |
+
- POSTGRES_USER=mep
|
| 24 |
+
- POSTGRES_PASSWORD=mep
|
| 25 |
+
- POSTGRES_DB=mep
|
| 26 |
+
volumes:
|
| 27 |
+
- ./hub_data/pgdata:/var/lib/postgresql/data
|
| 28 |
+
healthcheck:
|
| 29 |
+
test: ["CMD-SHELL", "pg_isready -U mep -d mep"]
|
| 30 |
+
interval: 10s
|
| 31 |
+
timeout: 5s
|
| 32 |
+
retries: 5
|
hub/db.py
CHANGED
|
@@ -1,13 +1,51 @@
|
|
| 1 |
import sqlite3
|
|
|
|
|
|
|
| 2 |
from typing import Optional
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
def init_db():
|
| 7 |
-
conn =
|
| 8 |
cursor = conn.cursor()
|
| 9 |
-
# Drop existing table to upgrade schema for Crypto Auth
|
| 10 |
-
cursor.execute("DROP TABLE IF EXISTS ledger")
|
| 11 |
cursor.execute('''
|
| 12 |
CREATE TABLE IF NOT EXISTS ledger (
|
| 13 |
node_id TEXT PRIMARY KEY,
|
|
@@ -15,66 +53,247 @@ def init_db():
|
|
| 15 |
balance REAL NOT NULL
|
| 16 |
)
|
| 17 |
''')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
conn.commit()
|
| 19 |
-
|
| 20 |
|
| 21 |
def register_node(node_id: str, pub_pem: str) -> float:
|
| 22 |
-
conn =
|
| 23 |
cursor = conn.cursor()
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
row = cursor.fetchone()
|
| 26 |
if not row:
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
conn.commit()
|
| 29 |
-
|
|
|
|
| 30 |
else:
|
| 31 |
-
balance =
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
|
| 35 |
def get_pub_pem(node_id: str) -> Optional[str]:
|
| 36 |
-
conn =
|
| 37 |
cursor = conn.cursor()
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
row = cursor.fetchone()
|
| 40 |
-
|
| 41 |
return row[0] if row else None
|
| 42 |
|
| 43 |
def get_balance(node_id: str) -> Optional[float]:
|
| 44 |
-
conn =
|
| 45 |
cursor = conn.cursor()
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
row = cursor.fetchone()
|
| 48 |
-
|
| 49 |
return row[0] if row else None
|
| 50 |
|
| 51 |
def set_balance(node_id: str, balance: float):
|
| 52 |
-
|
| 53 |
-
conn = sqlite3.connect(DB_FILE)
|
| 54 |
cursor = conn.cursor()
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
| 56 |
conn.commit()
|
| 57 |
-
|
| 58 |
|
| 59 |
def add_balance(node_id: str, amount: float):
|
| 60 |
-
conn =
|
| 61 |
cursor = conn.cursor()
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
conn.commit()
|
| 64 |
-
|
| 65 |
|
| 66 |
def deduct_balance(node_id: str, amount: float) -> bool:
|
| 67 |
-
conn =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
cursor = conn.cursor()
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
row = cursor.fetchone()
|
| 71 |
-
if
|
| 72 |
-
|
| 73 |
-
return
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
conn.commit()
|
| 77 |
-
|
| 78 |
-
return True
|
| 79 |
|
| 80 |
-
init_db()
|
|
|
|
| 1 |
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
from typing import Optional
|
| 5 |
|
| 6 |
+
try:
|
| 7 |
+
import psycopg2
|
| 8 |
+
from psycopg2 import pool
|
| 9 |
+
except ImportError:
|
| 10 |
+
psycopg2 = None
|
| 11 |
+
|
| 12 |
+
DB_FILE = os.getenv("MEP_SQLITE_PATH", "ledger.db")
|
| 13 |
+
DB_URL = os.getenv("MEP_DATABASE_URL")
|
| 14 |
+
PG_POOL_MIN = int(os.getenv("MEP_PG_POOL_MIN", "1"))
|
| 15 |
+
PG_POOL_MAX = int(os.getenv("MEP_PG_POOL_MAX", "5"))
|
| 16 |
+
_pg_pool: Optional["pool.SimpleConnectionPool"] = None
|
| 17 |
+
|
| 18 |
+
def _is_postgres() -> bool:
|
| 19 |
+
return bool(DB_URL)
|
| 20 |
+
|
| 21 |
+
def _get_pg_pool():
|
| 22 |
+
global _pg_pool
|
| 23 |
+
if _pg_pool is None:
|
| 24 |
+
if psycopg2 is None:
|
| 25 |
+
raise RuntimeError("psycopg2 is required for Postgres")
|
| 26 |
+
_pg_pool = pool.SimpleConnectionPool(PG_POOL_MIN, PG_POOL_MAX, DB_URL)
|
| 27 |
+
return _pg_pool
|
| 28 |
+
|
| 29 |
+
def _get_conn():
|
| 30 |
+
if _is_postgres():
|
| 31 |
+
return _get_pg_pool().getconn()
|
| 32 |
+
return sqlite3.connect(DB_FILE, check_same_thread=False)
|
| 33 |
+
|
| 34 |
+
def _release_conn(conn):
|
| 35 |
+
if _is_postgres():
|
| 36 |
+
_get_pg_pool().putconn(conn)
|
| 37 |
+
else:
|
| 38 |
+
conn.close()
|
| 39 |
+
|
| 40 |
+
def _row_to_dict(cursor, row):
|
| 41 |
+
if row is None:
|
| 42 |
+
return None
|
| 43 |
+
columns = [desc[0] for desc in cursor.description]
|
| 44 |
+
return dict(zip(columns, row))
|
| 45 |
|
| 46 |
def init_db():
|
| 47 |
+
conn = _get_conn()
|
| 48 |
cursor = conn.cursor()
|
|
|
|
|
|
|
| 49 |
cursor.execute('''
|
| 50 |
CREATE TABLE IF NOT EXISTS ledger (
|
| 51 |
node_id TEXT PRIMARY KEY,
|
|
|
|
| 53 |
balance REAL NOT NULL
|
| 54 |
)
|
| 55 |
''')
|
| 56 |
+
cursor.execute('''
|
| 57 |
+
CREATE TABLE IF NOT EXISTS tasks (
|
| 58 |
+
task_id TEXT PRIMARY KEY,
|
| 59 |
+
consumer_id TEXT NOT NULL,
|
| 60 |
+
provider_id TEXT,
|
| 61 |
+
payload TEXT NOT NULL,
|
| 62 |
+
bounty REAL NOT NULL,
|
| 63 |
+
status TEXT NOT NULL,
|
| 64 |
+
target_node TEXT,
|
| 65 |
+
model_requirement TEXT,
|
| 66 |
+
result_payload TEXT,
|
| 67 |
+
created_at REAL NOT NULL,
|
| 68 |
+
updated_at REAL NOT NULL
|
| 69 |
+
)
|
| 70 |
+
''')
|
| 71 |
+
cursor.execute('''
|
| 72 |
+
CREATE TABLE IF NOT EXISTS idempotency (
|
| 73 |
+
node_id TEXT NOT NULL,
|
| 74 |
+
endpoint TEXT NOT NULL,
|
| 75 |
+
idem_key TEXT NOT NULL,
|
| 76 |
+
response TEXT NOT NULL,
|
| 77 |
+
status_code INTEGER NOT NULL,
|
| 78 |
+
created_at REAL NOT NULL,
|
| 79 |
+
PRIMARY KEY (node_id, endpoint, idem_key)
|
| 80 |
+
)
|
| 81 |
+
''')
|
| 82 |
conn.commit()
|
| 83 |
+
_release_conn(conn)
|
| 84 |
|
| 85 |
def register_node(node_id: str, pub_pem: str) -> float:
|
| 86 |
+
conn = _get_conn()
|
| 87 |
cursor = conn.cursor()
|
| 88 |
+
if _is_postgres():
|
| 89 |
+
cursor.execute("SELECT balance FROM ledger WHERE node_id = %s", (node_id,))
|
| 90 |
+
else:
|
| 91 |
+
cursor.execute("SELECT balance FROM ledger WHERE node_id = ?", (node_id,))
|
| 92 |
row = cursor.fetchone()
|
| 93 |
if not row:
|
| 94 |
+
if _is_postgres():
|
| 95 |
+
cursor.execute(
|
| 96 |
+
"INSERT INTO ledger (node_id, pub_pem, balance) VALUES (%s, %s, %s) ON CONFLICT (node_id) DO NOTHING",
|
| 97 |
+
(node_id, pub_pem, 10.0)
|
| 98 |
+
)
|
| 99 |
+
else:
|
| 100 |
+
cursor.execute(
|
| 101 |
+
"INSERT OR IGNORE INTO ledger (node_id, pub_pem, balance) VALUES (?, ?, ?)",
|
| 102 |
+
(node_id, pub_pem, 10.0)
|
| 103 |
+
)
|
| 104 |
conn.commit()
|
| 105 |
+
if _is_postgres():
|
| 106 |
+
cursor.execute("SELECT balance FROM ledger WHERE node_id = %s", (node_id,))
|
| 107 |
else:
|
| 108 |
+
cursor.execute("SELECT balance FROM ledger WHERE node_id = ?", (node_id,))
|
| 109 |
+
row = cursor.fetchone()
|
| 110 |
+
_release_conn(conn)
|
| 111 |
+
return row[0] if row else 10.0
|
| 112 |
|
| 113 |
def get_pub_pem(node_id: str) -> Optional[str]:
|
| 114 |
+
conn = _get_conn()
|
| 115 |
cursor = conn.cursor()
|
| 116 |
+
if _is_postgres():
|
| 117 |
+
cursor.execute("SELECT pub_pem FROM ledger WHERE node_id = %s", (node_id,))
|
| 118 |
+
else:
|
| 119 |
+
cursor.execute("SELECT pub_pem FROM ledger WHERE node_id = ?", (node_id,))
|
| 120 |
row = cursor.fetchone()
|
| 121 |
+
_release_conn(conn)
|
| 122 |
return row[0] if row else None
|
| 123 |
|
| 124 |
def get_balance(node_id: str) -> Optional[float]:
|
| 125 |
+
conn = _get_conn()
|
| 126 |
cursor = conn.cursor()
|
| 127 |
+
if _is_postgres():
|
| 128 |
+
cursor.execute("SELECT balance FROM ledger WHERE node_id = %s", (node_id,))
|
| 129 |
+
else:
|
| 130 |
+
cursor.execute("SELECT balance FROM ledger WHERE node_id = ?", (node_id,))
|
| 131 |
row = cursor.fetchone()
|
| 132 |
+
_release_conn(conn)
|
| 133 |
return row[0] if row else None
|
| 134 |
|
| 135 |
def set_balance(node_id: str, balance: float):
|
| 136 |
+
conn = _get_conn()
|
|
|
|
| 137 |
cursor = conn.cursor()
|
| 138 |
+
if _is_postgres():
|
| 139 |
+
cursor.execute("UPDATE ledger SET balance = %s WHERE node_id = %s", (balance, node_id))
|
| 140 |
+
else:
|
| 141 |
+
cursor.execute("UPDATE ledger SET balance = ? WHERE node_id = ?", (balance, node_id))
|
| 142 |
conn.commit()
|
| 143 |
+
_release_conn(conn)
|
| 144 |
|
| 145 |
def add_balance(node_id: str, amount: float):
|
| 146 |
+
conn = _get_conn()
|
| 147 |
cursor = conn.cursor()
|
| 148 |
+
if _is_postgres():
|
| 149 |
+
cursor.execute("UPDATE ledger SET balance = balance + %s WHERE node_id = %s", (amount, node_id))
|
| 150 |
+
else:
|
| 151 |
+
cursor.execute("UPDATE ledger SET balance = balance + ? WHERE node_id = ?", (amount, node_id))
|
| 152 |
conn.commit()
|
| 153 |
+
_release_conn(conn)
|
| 154 |
|
| 155 |
def deduct_balance(node_id: str, amount: float) -> bool:
|
| 156 |
+
conn = _get_conn()
|
| 157 |
+
cursor = conn.cursor()
|
| 158 |
+
if _is_postgres():
|
| 159 |
+
cursor.execute(
|
| 160 |
+
"UPDATE ledger SET balance = balance - %s WHERE node_id = %s AND balance >= %s",
|
| 161 |
+
(amount, node_id, amount)
|
| 162 |
+
)
|
| 163 |
+
else:
|
| 164 |
+
cursor.execute(
|
| 165 |
+
"UPDATE ledger SET balance = balance - ? WHERE node_id = ? AND balance >= ?",
|
| 166 |
+
(amount, node_id, amount)
|
| 167 |
+
)
|
| 168 |
+
updated = cursor.rowcount
|
| 169 |
+
conn.commit()
|
| 170 |
+
_release_conn(conn)
|
| 171 |
+
return updated > 0
|
| 172 |
+
|
| 173 |
+
def create_task(task_id: str, consumer_id: str, payload: str, bounty: float, status: str, target_node: Optional[str], model_requirement: Optional[str], created_at: float):
|
| 174 |
+
conn = _get_conn()
|
| 175 |
+
cursor = conn.cursor()
|
| 176 |
+
if _is_postgres():
|
| 177 |
+
cursor.execute(
|
| 178 |
+
"INSERT INTO tasks (task_id, consumer_id, provider_id, payload, bounty, status, target_node, model_requirement, result_payload, created_at, updated_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
| 179 |
+
(task_id, consumer_id, None, payload, bounty, status, target_node, model_requirement, None, created_at, created_at)
|
| 180 |
+
)
|
| 181 |
+
else:
|
| 182 |
+
cursor.execute(
|
| 183 |
+
"INSERT INTO tasks (task_id, consumer_id, provider_id, payload, bounty, status, target_node, model_requirement, result_payload, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
| 184 |
+
(task_id, consumer_id, None, payload, bounty, status, target_node, model_requirement, None, created_at, created_at)
|
| 185 |
+
)
|
| 186 |
+
conn.commit()
|
| 187 |
+
_release_conn(conn)
|
| 188 |
+
|
| 189 |
+
def update_task_assignment(task_id: str, provider_id: str, status: str, updated_at: float):
|
| 190 |
+
conn = _get_conn()
|
| 191 |
+
cursor = conn.cursor()
|
| 192 |
+
if _is_postgres():
|
| 193 |
+
cursor.execute(
|
| 194 |
+
"UPDATE tasks SET provider_id = %s, status = %s, updated_at = %s WHERE task_id = %s",
|
| 195 |
+
(provider_id, status, updated_at, task_id)
|
| 196 |
+
)
|
| 197 |
+
else:
|
| 198 |
+
cursor.execute(
|
| 199 |
+
"UPDATE tasks SET provider_id = ?, status = ?, updated_at = ? WHERE task_id = ?",
|
| 200 |
+
(provider_id, status, updated_at, task_id)
|
| 201 |
+
)
|
| 202 |
+
conn.commit()
|
| 203 |
+
_release_conn(conn)
|
| 204 |
+
|
| 205 |
+
def update_task_result(task_id: str, provider_id: str, result_payload: str, status: str, updated_at: float):
|
| 206 |
+
conn = _get_conn()
|
| 207 |
+
cursor = conn.cursor()
|
| 208 |
+
if _is_postgres():
|
| 209 |
+
cursor.execute(
|
| 210 |
+
"UPDATE tasks SET provider_id = %s, result_payload = %s, status = %s, updated_at = %s WHERE task_id = %s",
|
| 211 |
+
(provider_id, result_payload, status, updated_at, task_id)
|
| 212 |
+
)
|
| 213 |
+
else:
|
| 214 |
+
cursor.execute(
|
| 215 |
+
"UPDATE tasks SET provider_id = ?, result_payload = ?, status = ?, updated_at = ? WHERE task_id = ?",
|
| 216 |
+
(provider_id, result_payload, status, updated_at, task_id)
|
| 217 |
+
)
|
| 218 |
+
conn.commit()
|
| 219 |
+
_release_conn(conn)
|
| 220 |
+
|
| 221 |
+
def get_task(task_id: str) -> Optional[dict]:
|
| 222 |
+
conn = _get_conn()
|
| 223 |
+
if not _is_postgres():
|
| 224 |
+
conn.row_factory = sqlite3.Row
|
| 225 |
cursor = conn.cursor()
|
| 226 |
+
if _is_postgres():
|
| 227 |
+
cursor.execute("SELECT * FROM tasks WHERE task_id = %s", (task_id,))
|
| 228 |
+
else:
|
| 229 |
+
cursor.execute("SELECT * FROM tasks WHERE task_id = ?", (task_id,))
|
| 230 |
row = cursor.fetchone()
|
| 231 |
+
if not row:
|
| 232 |
+
_release_conn(conn)
|
| 233 |
+
return None
|
| 234 |
+
if _is_postgres():
|
| 235 |
+
result = _row_to_dict(cursor, row)
|
| 236 |
+
_release_conn(conn)
|
| 237 |
+
return result
|
| 238 |
+
result = dict(row)
|
| 239 |
+
_release_conn(conn)
|
| 240 |
+
return result
|
| 241 |
+
|
| 242 |
+
def get_active_tasks() -> list:
|
| 243 |
+
conn = _get_conn()
|
| 244 |
+
if not _is_postgres():
|
| 245 |
+
conn.row_factory = sqlite3.Row
|
| 246 |
+
cursor = conn.cursor()
|
| 247 |
+
if _is_postgres():
|
| 248 |
+
cursor.execute("SELECT * FROM tasks WHERE status IN ('bidding', 'assigned')")
|
| 249 |
+
else:
|
| 250 |
+
cursor.execute("SELECT * FROM tasks WHERE status IN ('bidding', 'assigned')")
|
| 251 |
+
rows = cursor.fetchall()
|
| 252 |
+
if _is_postgres():
|
| 253 |
+
result = [_row_to_dict(cursor, row) for row in rows]
|
| 254 |
+
_release_conn(conn)
|
| 255 |
+
return result
|
| 256 |
+
result = [dict(row) for row in rows]
|
| 257 |
+
_release_conn(conn)
|
| 258 |
+
return result
|
| 259 |
+
|
| 260 |
+
def get_idempotency(node_id: str, endpoint: str, idem_key: str) -> Optional[dict]:
|
| 261 |
+
conn = _get_conn()
|
| 262 |
+
cursor = conn.cursor()
|
| 263 |
+
if _is_postgres():
|
| 264 |
+
cursor.execute(
|
| 265 |
+
"SELECT response, status_code FROM idempotency WHERE node_id = %s AND endpoint = %s AND idem_key = %s",
|
| 266 |
+
(node_id, endpoint, idem_key)
|
| 267 |
+
)
|
| 268 |
+
else:
|
| 269 |
+
cursor.execute(
|
| 270 |
+
"SELECT response, status_code FROM idempotency WHERE node_id = ? AND endpoint = ? AND idem_key = ?",
|
| 271 |
+
(node_id, endpoint, idem_key)
|
| 272 |
+
)
|
| 273 |
+
row = cursor.fetchone()
|
| 274 |
+
if not row:
|
| 275 |
+
_release_conn(conn)
|
| 276 |
+
return None
|
| 277 |
+
response = json.loads(row[0])
|
| 278 |
+
result = {"response": response, "status_code": row[1]}
|
| 279 |
+
_release_conn(conn)
|
| 280 |
+
return result
|
| 281 |
+
|
| 282 |
+
def set_idempotency(node_id: str, endpoint: str, idem_key: str, response: dict, status_code: int, created_at: float):
|
| 283 |
+
conn = _get_conn()
|
| 284 |
+
cursor = conn.cursor()
|
| 285 |
+
payload = json.dumps(response)
|
| 286 |
+
if _is_postgres():
|
| 287 |
+
cursor.execute(
|
| 288 |
+
"INSERT INTO idempotency (node_id, endpoint, idem_key, response, status_code, created_at) VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT (node_id, endpoint, idem_key) DO NOTHING",
|
| 289 |
+
(node_id, endpoint, idem_key, payload, status_code, created_at)
|
| 290 |
+
)
|
| 291 |
+
else:
|
| 292 |
+
cursor.execute(
|
| 293 |
+
"INSERT OR IGNORE INTO idempotency (node_id, endpoint, idem_key, response, status_code, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
| 294 |
+
(node_id, endpoint, idem_key, payload, status_code, created_at)
|
| 295 |
+
)
|
| 296 |
conn.commit()
|
| 297 |
+
_release_conn(conn)
|
|
|
|
| 298 |
|
| 299 |
+
init_db()
|
hub/ledger.db
DELETED
|
Binary file (12.3 kB)
|
|
|
hub/logs/hub.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
{"timestamp": "2026-02-24T07:15:32.589565+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_7c115a964de4 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_7c115a964de4", "starting_balance": 10.0}
|
| 2 |
-
{"timestamp": "2026-02-24T07:15:32.603735+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_31a01d787f88 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_31a01d787f88", "starting_balance": 10.0}
|
| 3 |
-
{"timestamp": "2026-02-24T07:16:06.691635+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_7c115a964de4 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_7c115a964de4", "starting_balance": 10.0}
|
| 4 |
-
{"timestamp": "2026-02-24T07:16:06.711586+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_31a01d787f88 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_31a01d787f88", "starting_balance": 10.0}
|
| 5 |
-
{"timestamp": "2026-02-24T07:16:23.994625+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_7c115a964de4 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_7c115a964de4", "starting_balance": 10.0}
|
| 6 |
-
{"timestamp": "2026-02-24T07:16:24.007658+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_31a01d787f88 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_31a01d787f88", "starting_balance": 10.0}
|
| 7 |
-
{"timestamp": "2026-02-24T07:16:24.573446+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task c9d51463 broadcasted by node_7c115a964de4 for 5.0", "event": "task_submitted", "consumer_id": "node_7c115a964de4", "task_id": "c9d51463-1d89-4aa0-9899-be5090bc4edf", "bounty": 5.0}
|
| 8 |
-
{"timestamp": "2026-02-24T07:16:24.582713+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task c9d51463 assigned to node_31a01d787f88", "event": "bid_accepted", "task_id": "c9d51463-1d89-4aa0-9899-be5090bc4edf", "provider_id": "node_31a01d787f88", "bounty": 5.0}
|
| 9 |
-
{"timestamp": "2026-02-24T07:16:24.598410+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task c9d51463 completed by node_31a01d787f88", "event": "task_completed", "task_id": "c9d51463-1d89-4aa0-9899-be5090bc4edf", "provider_id": "node_31a01d787f88", "bounty": 5.0}
|
| 10 |
-
{"timestamp": "2026-02-24T07:16:24.608789+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task 4f2e4436 broadcasted by node_7c115a964de4 for 0.0", "event": "task_submitted", "consumer_id": "node_7c115a964de4", "task_id": "4f2e4436-abff-4c7e-8777-2b8326c824ed", "bounty": 0.0}
|
| 11 |
-
{"timestamp": "2026-02-24T07:16:24.617370+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task 4f2e4436 completed by node_31a01d787f88", "event": "task_completed", "task_id": "4f2e4436-abff-4c7e-8777-2b8326c824ed", "provider_id": "node_31a01d787f88", "bounty": 0.0}
|
| 12 |
-
{"timestamp": "2026-02-24T07:16:24.625135+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task b9c676c9 broadcasted by node_7c115a964de4 for -2.0", "event": "task_submitted", "consumer_id": "node_7c115a964de4", "task_id": "b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35", "bounty": -2.0}
|
| 13 |
-
{"timestamp": "2026-02-24T07:16:24.632191+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task b9c676c9 assigned to node_31a01d787f88", "event": "bid_accepted", "task_id": "b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35", "provider_id": "node_31a01d787f88", "bounty": -2.0}
|
| 14 |
-
{"timestamp": "2026-02-24T07:16:24.651742+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task b9c676c9 completed by node_31a01d787f88", "event": "task_completed", "task_id": "b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35", "provider_id": "node_31a01d787f88", "bounty": -2.0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hub/logs/ledger_audit.log
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
2026-02-24 07:15:32,590 - mep.audit - INFO - AUDIT | REGISTER | Node: node_7c115a964de4 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
|
| 2 |
-
2026-02-24 07:15:32,604 - mep.audit - INFO - AUDIT | REGISTER | Node: node_31a01d787f88 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
|
| 3 |
-
2026-02-24 07:16:06,692 - mep.audit - INFO - AUDIT | REGISTER | Node: node_7c115a964de4 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
|
| 4 |
-
2026-02-24 07:16:06,712 - mep.audit - INFO - AUDIT | REGISTER | Node: node_31a01d787f88 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
|
| 5 |
-
2026-02-24 07:16:23,995 - mep.audit - INFO - AUDIT | REGISTER | Node: node_7c115a964de4 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
|
| 6 |
-
2026-02-24 07:16:24,008 - mep.audit - INFO - AUDIT | REGISTER | Node: node_31a01d787f88 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
|
| 7 |
-
2026-02-24 07:16:24,573 - mep.audit - INFO - AUDIT | ESCROW | Node: node_7c115a964de4 | Amount: -5.000000 | Balance: 5.000000 | Ref: c9d51463-1d89-4aa0-9899-be5090bc4edf
|
| 8 |
-
2026-02-24 07:16:24,598 - mep.audit - INFO - AUDIT | EARN_COMPUTE | Node: node_31a01d787f88 | Amount: +5.000000 | Balance: 15.000000 | Ref: c9d51463-1d89-4aa0-9899-be5090bc4edf
|
| 9 |
-
2026-02-24 07:16:24,617 - mep.audit - INFO - AUDIT | EARN_COMPUTE | Node: node_31a01d787f88 | Amount: +0.000000 | Balance: 15.000000 | Ref: 4f2e4436-abff-4c7e-8777-2b8326c824ed
|
| 10 |
-
2026-02-24 07:16:24,645 - mep.audit - INFO - AUDIT | BUY_DATA | Node: node_31a01d787f88 | Amount: -2.000000 | Balance: 13.000000 | Ref: b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35
|
| 11 |
-
2026-02-24 07:16:24,651 - mep.audit - INFO - AUDIT | SELL_DATA | Node: node_7c115a964de4 | Amount: +2.000000 | Balance: 7.000000 | Ref: b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hub/main.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
-
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Request, Header
|
| 2 |
-
from typing import Dict, List
|
| 3 |
import uuid
|
|
|
|
|
|
|
| 4 |
import db
|
| 5 |
import auth
|
| 6 |
-
from logger import log_event, log_audit
|
| 7 |
|
| 8 |
-
from models import NodeRegistration, TaskCreate, TaskResult,
|
| 9 |
|
| 10 |
app = FastAPI(title="Chronos Protocol L1 Hub", description="The Time Exchange Clearinghouse", version="0.1.2")
|
| 11 |
|
|
@@ -13,15 +15,68 @@ app = FastAPI(title="Chronos Protocol L1 Hub", description="The Time Exchange Cl
|
|
| 13 |
active_tasks: Dict[str, dict] = {} # task_id -> task_details
|
| 14 |
completed_tasks: Dict[str, dict] = {} # task_id -> result
|
| 15 |
connected_nodes: Dict[str, WebSocket] = {} # node_id -> websocket
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
# --- IDENTITY VERIFICATION MIDDLEWARE ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
async def verify_request(
|
| 19 |
request: Request,
|
| 20 |
x_mep_nodeid: str = Header(...),
|
| 21 |
x_mep_timestamp: str = Header(...),
|
| 22 |
x_mep_signature: str = Header(...)
|
| 23 |
) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
body = await request.body()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
payload_str = body.decode('utf-8')
|
| 26 |
|
| 27 |
pub_pem = db.get_pub_pem(x_mep_nodeid)
|
|
@@ -34,7 +89,11 @@ async def verify_request(
|
|
| 34 |
return x_mep_nodeid
|
| 35 |
|
| 36 |
@app.post("/register")
|
| 37 |
-
async def register_node(node: NodeRegistration):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
# Registration derives the Node ID from the provided Public Key PEM
|
| 39 |
node_id = auth.derive_node_id(node.pubkey)
|
| 40 |
balance = db.register_node(node_id, node.pubkey)
|
|
@@ -51,14 +110,24 @@ async def get_balance(node_id: str):
|
|
| 51 |
raise HTTPException(status_code=404, detail="Node not found")
|
| 52 |
return {"node_id": node_id, "balance_seconds": balance}
|
| 53 |
|
| 54 |
-
from fastapi import Depends
|
| 55 |
-
|
| 56 |
@app.post("/tasks/submit")
|
| 57 |
-
async def submit_task(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
# Verify the signer is actually the consumer claiming to submit the task
|
| 59 |
if authenticated_node != task.consumer_id:
|
| 60 |
raise HTTPException(status_code=403, detail="Cannot submit tasks on behalf of another node")
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
consumer_balance = db.get_balance(task.consumer_id)
|
| 63 |
if consumer_balance is None:
|
| 64 |
raise HTTPException(status_code=404, detail="Consumer node not found")
|
|
@@ -68,6 +137,7 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
|
|
| 68 |
raise HTTPException(status_code=400, detail="Insufficient SECONDS balance to pay for task")
|
| 69 |
|
| 70 |
task_id = str(uuid.uuid4())
|
|
|
|
| 71 |
|
| 72 |
# Note: If bounty is negative, consumer is SELLING data. We don't deduct here.
|
| 73 |
# We will deduct from the provider when they complete the task.
|
|
@@ -91,6 +161,7 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
|
|
| 91 |
"target_node": task.target_node,
|
| 92 |
"model_requirement": task.model_requirement
|
| 93 |
}
|
|
|
|
| 94 |
active_tasks[task_id] = task_data
|
| 95 |
|
| 96 |
# Target specific node if requested (Direct Message skips bidding)
|
|
@@ -99,9 +170,13 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
|
|
| 99 |
try:
|
| 100 |
task_data["status"] = "assigned"
|
| 101 |
task_data["provider_id"] = task.target_node
|
|
|
|
| 102 |
await connected_nodes[task.target_node].send_json({"event": "new_task", "data": task_data})
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
| 105 |
return {"status": "error", "detail": "Target node disconnected"}
|
| 106 |
else:
|
| 107 |
return {"status": "error", "detail": "Target node not currently connected to Hub"}
|
|
@@ -117,10 +192,13 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
|
|
| 117 |
if node_id != task.consumer_id:
|
| 118 |
try:
|
| 119 |
await ws.send_json({"event": "rfc", "data": rfc_data})
|
| 120 |
-
except:
|
| 121 |
pass
|
| 122 |
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
@app.post("/tasks/bid")
|
| 126 |
async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_request)):
|
|
@@ -137,6 +215,7 @@ async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_reque
|
|
| 137 |
# Phase 2 Fast Auction: Accept the first valid bid
|
| 138 |
task["status"] = "assigned"
|
| 139 |
task["provider_id"] = bid.provider_id
|
|
|
|
| 140 |
|
| 141 |
log_event("bid_accepted", f"Task {bid.task_id[:8]} assigned to {bid.provider_id}", task_id=bid.task_id, provider_id=bid.provider_id, bounty=task["bounty"])
|
| 142 |
|
|
@@ -149,13 +228,38 @@ async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_reque
|
|
| 149 |
}
|
| 150 |
|
| 151 |
@app.post("/tasks/complete")
|
| 152 |
-
async def complete_task(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
if authenticated_node != result.provider_id:
|
| 154 |
raise HTTPException(status_code=403, detail="Cannot complete tasks on behalf of another node")
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
task = active_tasks.get(result.task_id)
|
| 157 |
if not task:
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
provider_balance = db.get_balance(result.provider_id)
|
| 161 |
if provider_balance is None:
|
|
@@ -191,6 +295,7 @@ async def complete_task(result: TaskResult, authenticated_node: str = Depends(ve
|
|
| 191 |
task["result"] = result.result_payload
|
| 192 |
completed_tasks[result.task_id] = task
|
| 193 |
del active_tasks[result.task_id]
|
|
|
|
| 194 |
|
| 195 |
# ROUTE RESULT BACK TO CONSUMER VIA WEBSOCKET
|
| 196 |
consumer_id = task["consumer_id"]
|
|
@@ -205,13 +310,47 @@ async def complete_task(result: TaskResult, authenticated_node: str = Depends(ve
|
|
| 205 |
"bounty_spent": task["bounty"]
|
| 206 |
}
|
| 207 |
})
|
| 208 |
-
except:
|
| 209 |
pass # Consumer disconnected, they can fetch it via REST later (TODO)
|
| 210 |
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
@app.websocket("/ws/{node_id}")
|
| 214 |
async def websocket_endpoint(websocket: WebSocket, node_id: str, timestamp: str, signature: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
pub_pem = db.get_pub_pem(node_id)
|
| 216 |
if not pub_pem:
|
| 217 |
await websocket.close(code=4001, reason="Unknown Node ID")
|
|
@@ -225,7 +364,7 @@ async def websocket_endpoint(websocket: WebSocket, node_id: str, timestamp: str,
|
|
| 225 |
connected_nodes[node_id] = websocket
|
| 226 |
try:
|
| 227 |
while True:
|
| 228 |
-
|
| 229 |
except WebSocketDisconnect:
|
| 230 |
if node_id in connected_nodes:
|
| 231 |
del connected_nodes[node_id]
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Request, Header, Depends
|
| 2 |
+
from typing import Dict, List, Optional
|
| 3 |
import uuid
|
| 4 |
+
import time
|
| 5 |
+
import os
|
| 6 |
import db
|
| 7 |
import auth
|
| 8 |
+
from logger import log_event, log_audit
|
| 9 |
|
| 10 |
+
from models import NodeRegistration, TaskCreate, TaskResult, TaskBid
|
| 11 |
|
| 12 |
app = FastAPI(title="Chronos Protocol L1 Hub", description="The Time Exchange Clearinghouse", version="0.1.2")
|
| 13 |
|
|
|
|
| 15 |
active_tasks: Dict[str, dict] = {} # task_id -> task_details
|
| 16 |
completed_tasks: Dict[str, dict] = {} # task_id -> result
|
| 17 |
connected_nodes: Dict[str, WebSocket] = {} # node_id -> websocket
|
| 18 |
+
rate_limits: Dict[str, List[float]] = {}
|
| 19 |
+
MAX_BODY_BYTES = 200_000
|
| 20 |
+
MAX_PAYLOAD_CHARS = 20_000
|
| 21 |
+
RATE_LIMIT_WINDOW = 10.0
|
| 22 |
+
RATE_LIMIT_MAX = 50
|
| 23 |
+
MAX_SKEW_SECONDS = 300
|
| 24 |
+
ALLOWED_IPS = [ip.strip() for ip in os.getenv("MEP_ALLOWED_IPS", "").split(",") if ip.strip()]
|
| 25 |
+
for task in db.get_active_tasks():
|
| 26 |
+
task_data = {
|
| 27 |
+
"id": task["task_id"],
|
| 28 |
+
"consumer_id": task["consumer_id"],
|
| 29 |
+
"payload": task["payload"],
|
| 30 |
+
"bounty": task["bounty"],
|
| 31 |
+
"status": task["status"],
|
| 32 |
+
"target_node": task["target_node"],
|
| 33 |
+
"model_requirement": task["model_requirement"],
|
| 34 |
+
"provider_id": task["provider_id"]
|
| 35 |
+
}
|
| 36 |
+
active_tasks[task_data["id"]] = task_data
|
| 37 |
|
| 38 |
# --- IDENTITY VERIFICATION MIDDLEWARE ---
|
| 39 |
+
def _is_allowed_ip(host: Optional[str]) -> bool:
|
| 40 |
+
if not ALLOWED_IPS:
|
| 41 |
+
return True
|
| 42 |
+
return host in ALLOWED_IPS
|
| 43 |
+
|
| 44 |
+
def _apply_rate_limit(key: str):
|
| 45 |
+
now = time.time()
|
| 46 |
+
window_start = now - RATE_LIMIT_WINDOW
|
| 47 |
+
timestamps = rate_limits.get(key, [])
|
| 48 |
+
timestamps = [t for t in timestamps if t >= window_start]
|
| 49 |
+
if len(timestamps) >= RATE_LIMIT_MAX:
|
| 50 |
+
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
| 51 |
+
timestamps.append(now)
|
| 52 |
+
rate_limits[key] = timestamps
|
| 53 |
+
|
| 54 |
+
def _validate_timestamp(ts: str):
|
| 55 |
+
try:
|
| 56 |
+
ts_int = int(ts)
|
| 57 |
+
except ValueError:
|
| 58 |
+
raise HTTPException(status_code=400, detail="Invalid timestamp")
|
| 59 |
+
now = int(time.time())
|
| 60 |
+
if abs(now - ts_int) > MAX_SKEW_SECONDS:
|
| 61 |
+
raise HTTPException(status_code=401, detail="Timestamp out of allowed window")
|
| 62 |
+
|
| 63 |
async def verify_request(
|
| 64 |
request: Request,
|
| 65 |
x_mep_nodeid: str = Header(...),
|
| 66 |
x_mep_timestamp: str = Header(...),
|
| 67 |
x_mep_signature: str = Header(...)
|
| 68 |
) -> str:
|
| 69 |
+
client_host = request.client.host if request.client else None
|
| 70 |
+
if not _is_allowed_ip(client_host):
|
| 71 |
+
raise HTTPException(status_code=403, detail="Client IP not allowed")
|
| 72 |
+
|
| 73 |
body = await request.body()
|
| 74 |
+
if len(body) > MAX_BODY_BYTES:
|
| 75 |
+
raise HTTPException(status_code=413, detail="Payload too large")
|
| 76 |
+
|
| 77 |
+
_apply_rate_limit(f"{x_mep_nodeid}:{request.url.path}")
|
| 78 |
+
_validate_timestamp(x_mep_timestamp)
|
| 79 |
+
|
| 80 |
payload_str = body.decode('utf-8')
|
| 81 |
|
| 82 |
pub_pem = db.get_pub_pem(x_mep_nodeid)
|
|
|
|
| 89 |
return x_mep_nodeid
|
| 90 |
|
| 91 |
@app.post("/register")
|
| 92 |
+
async def register_node(node: NodeRegistration, request: Request):
|
| 93 |
+
client_host = request.client.host if request.client else None
|
| 94 |
+
if not _is_allowed_ip(client_host):
|
| 95 |
+
raise HTTPException(status_code=403, detail="Client IP not allowed")
|
| 96 |
+
_apply_rate_limit(f"{client_host}:/register")
|
| 97 |
# Registration derives the Node ID from the provided Public Key PEM
|
| 98 |
node_id = auth.derive_node_id(node.pubkey)
|
| 99 |
balance = db.register_node(node_id, node.pubkey)
|
|
|
|
| 110 |
raise HTTPException(status_code=404, detail="Node not found")
|
| 111 |
return {"node_id": node_id, "balance_seconds": balance}
|
| 112 |
|
|
|
|
|
|
|
| 113 |
@app.post("/tasks/submit")
|
| 114 |
+
async def submit_task(
|
| 115 |
+
task: TaskCreate,
|
| 116 |
+
authenticated_node: str = Depends(verify_request),
|
| 117 |
+
x_mep_idempotency_key: Optional[str] = Header(default=None)
|
| 118 |
+
):
|
| 119 |
# Verify the signer is actually the consumer claiming to submit the task
|
| 120 |
if authenticated_node != task.consumer_id:
|
| 121 |
raise HTTPException(status_code=403, detail="Cannot submit tasks on behalf of another node")
|
| 122 |
|
| 123 |
+
if len(task.payload) > MAX_PAYLOAD_CHARS:
|
| 124 |
+
raise HTTPException(status_code=413, detail="Task payload too large")
|
| 125 |
+
|
| 126 |
+
if x_mep_idempotency_key:
|
| 127 |
+
existing = db.get_idempotency(authenticated_node, "/tasks/submit", x_mep_idempotency_key)
|
| 128 |
+
if existing:
|
| 129 |
+
return existing["response"]
|
| 130 |
+
|
| 131 |
consumer_balance = db.get_balance(task.consumer_id)
|
| 132 |
if consumer_balance is None:
|
| 133 |
raise HTTPException(status_code=404, detail="Consumer node not found")
|
|
|
|
| 137 |
raise HTTPException(status_code=400, detail="Insufficient SECONDS balance to pay for task")
|
| 138 |
|
| 139 |
task_id = str(uuid.uuid4())
|
| 140 |
+
now = time.time()
|
| 141 |
|
| 142 |
# Note: If bounty is negative, consumer is SELLING data. We don't deduct here.
|
| 143 |
# We will deduct from the provider when they complete the task.
|
|
|
|
| 161 |
"target_node": task.target_node,
|
| 162 |
"model_requirement": task.model_requirement
|
| 163 |
}
|
| 164 |
+
db.create_task(task_id, task.consumer_id, task.payload, task.bounty, "bidding", task.target_node, task.model_requirement, now)
|
| 165 |
active_tasks[task_id] = task_data
|
| 166 |
|
| 167 |
# Target specific node if requested (Direct Message skips bidding)
|
|
|
|
| 170 |
try:
|
| 171 |
task_data["status"] = "assigned"
|
| 172 |
task_data["provider_id"] = task.target_node
|
| 173 |
+
db.update_task_assignment(task_id, task.target_node, "assigned", time.time())
|
| 174 |
await connected_nodes[task.target_node].send_json({"event": "new_task", "data": task_data})
|
| 175 |
+
response_payload = {"status": "success", "task_id": task_id, "routed_to": task.target_node}
|
| 176 |
+
if x_mep_idempotency_key:
|
| 177 |
+
db.set_idempotency(authenticated_node, "/tasks/submit", x_mep_idempotency_key, response_payload, 200, time.time())
|
| 178 |
+
return response_payload
|
| 179 |
+
except Exception:
|
| 180 |
return {"status": "error", "detail": "Target node disconnected"}
|
| 181 |
else:
|
| 182 |
return {"status": "error", "detail": "Target node not currently connected to Hub"}
|
|
|
|
| 192 |
if node_id != task.consumer_id:
|
| 193 |
try:
|
| 194 |
await ws.send_json({"event": "rfc", "data": rfc_data})
|
| 195 |
+
except Exception:
|
| 196 |
pass
|
| 197 |
|
| 198 |
+
response_payload = {"status": "success", "task_id": task_id}
|
| 199 |
+
if x_mep_idempotency_key:
|
| 200 |
+
db.set_idempotency(authenticated_node, "/tasks/submit", x_mep_idempotency_key, response_payload, 200, time.time())
|
| 201 |
+
return response_payload
|
| 202 |
|
| 203 |
@app.post("/tasks/bid")
|
| 204 |
async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_request)):
|
|
|
|
| 215 |
# Phase 2 Fast Auction: Accept the first valid bid
|
| 216 |
task["status"] = "assigned"
|
| 217 |
task["provider_id"] = bid.provider_id
|
| 218 |
+
db.update_task_assignment(bid.task_id, bid.provider_id, "assigned", time.time())
|
| 219 |
|
| 220 |
log_event("bid_accepted", f"Task {bid.task_id[:8]} assigned to {bid.provider_id}", task_id=bid.task_id, provider_id=bid.provider_id, bounty=task["bounty"])
|
| 221 |
|
|
|
|
| 228 |
}
|
| 229 |
|
| 230 |
@app.post("/tasks/complete")
|
| 231 |
+
async def complete_task(
|
| 232 |
+
result: TaskResult,
|
| 233 |
+
authenticated_node: str = Depends(verify_request),
|
| 234 |
+
x_mep_idempotency_key: Optional[str] = Header(default=None)
|
| 235 |
+
):
|
| 236 |
if authenticated_node != result.provider_id:
|
| 237 |
raise HTTPException(status_code=403, detail="Cannot complete tasks on behalf of another node")
|
| 238 |
|
| 239 |
+
if len(result.result_payload) > MAX_PAYLOAD_CHARS:
|
| 240 |
+
raise HTTPException(status_code=413, detail="Result payload too large")
|
| 241 |
+
|
| 242 |
+
if x_mep_idempotency_key:
|
| 243 |
+
existing = db.get_idempotency(authenticated_node, "/tasks/complete", x_mep_idempotency_key)
|
| 244 |
+
if existing:
|
| 245 |
+
return existing["response"]
|
| 246 |
+
|
| 247 |
task = active_tasks.get(result.task_id)
|
| 248 |
if not task:
|
| 249 |
+
db_task = db.get_task(result.task_id)
|
| 250 |
+
if not db_task or db_task["status"] not in ("bidding", "assigned"):
|
| 251 |
+
raise HTTPException(status_code=404, detail="Task not found or already claimed")
|
| 252 |
+
task = {
|
| 253 |
+
"id": db_task["task_id"],
|
| 254 |
+
"consumer_id": db_task["consumer_id"],
|
| 255 |
+
"payload": db_task["payload"],
|
| 256 |
+
"bounty": db_task["bounty"],
|
| 257 |
+
"status": db_task["status"],
|
| 258 |
+
"target_node": db_task["target_node"],
|
| 259 |
+
"model_requirement": db_task["model_requirement"],
|
| 260 |
+
"provider_id": db_task["provider_id"]
|
| 261 |
+
}
|
| 262 |
+
active_tasks[result.task_id] = task
|
| 263 |
|
| 264 |
provider_balance = db.get_balance(result.provider_id)
|
| 265 |
if provider_balance is None:
|
|
|
|
| 295 |
task["result"] = result.result_payload
|
| 296 |
completed_tasks[result.task_id] = task
|
| 297 |
del active_tasks[result.task_id]
|
| 298 |
+
db.update_task_result(result.task_id, result.provider_id, result.result_payload, "completed", time.time())
|
| 299 |
|
| 300 |
# ROUTE RESULT BACK TO CONSUMER VIA WEBSOCKET
|
| 301 |
consumer_id = task["consumer_id"]
|
|
|
|
| 310 |
"bounty_spent": task["bounty"]
|
| 311 |
}
|
| 312 |
})
|
| 313 |
+
except Exception:
|
| 314 |
pass # Consumer disconnected, they can fetch it via REST later (TODO)
|
| 315 |
|
| 316 |
+
response_payload = {"status": "success", "earned": task["bounty"], "new_balance": db.get_balance(result.provider_id)}
|
| 317 |
+
if x_mep_idempotency_key:
|
| 318 |
+
db.set_idempotency(authenticated_node, "/tasks/complete", x_mep_idempotency_key, response_payload, 200, time.time())
|
| 319 |
+
return response_payload
|
| 320 |
+
|
| 321 |
+
@app.get("/tasks/result/{task_id}")
|
| 322 |
+
async def get_task_result(task_id: str, authenticated_node: str = Depends(verify_request)):
|
| 323 |
+
task = db.get_task(task_id)
|
| 324 |
+
if not task or task["status"] != "completed":
|
| 325 |
+
raise HTTPException(status_code=404, detail="Task not found or not completed")
|
| 326 |
+
if authenticated_node not in (task["consumer_id"], task["provider_id"]):
|
| 327 |
+
raise HTTPException(status_code=403, detail="Not authorized to view this result")
|
| 328 |
+
return {
|
| 329 |
+
"task_id": task["task_id"],
|
| 330 |
+
"consumer_id": task["consumer_id"],
|
| 331 |
+
"provider_id": task["provider_id"],
|
| 332 |
+
"bounty": task["bounty"],
|
| 333 |
+
"result_payload": task["result_payload"]
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
@app.get("/health")
|
| 337 |
+
async def health_check():
|
| 338 |
+
return {"status": "ok"}
|
| 339 |
|
| 340 |
@app.websocket("/ws/{node_id}")
|
| 341 |
async def websocket_endpoint(websocket: WebSocket, node_id: str, timestamp: str, signature: str):
|
| 342 |
+
client_host = websocket.client.host if websocket.client else None
|
| 343 |
+
if not _is_allowed_ip(client_host):
|
| 344 |
+
await websocket.close(code=4003, reason="Client IP not allowed")
|
| 345 |
+
return
|
| 346 |
+
|
| 347 |
+
try:
|
| 348 |
+
_apply_rate_limit(f"{node_id}:/ws")
|
| 349 |
+
_validate_timestamp(timestamp)
|
| 350 |
+
except HTTPException as exc:
|
| 351 |
+
await websocket.close(code=4004, reason=exc.detail)
|
| 352 |
+
return
|
| 353 |
+
|
| 354 |
pub_pem = db.get_pub_pem(node_id)
|
| 355 |
if not pub_pem:
|
| 356 |
await websocket.close(code=4001, reason="Unknown Node ID")
|
|
|
|
| 364 |
connected_nodes[node_id] = websocket
|
| 365 |
try:
|
| 366 |
while True:
|
| 367 |
+
await websocket.receive_text()
|
| 368 |
except WebSocketDisconnect:
|
| 369 |
if node_id in connected_nodes:
|
| 370 |
del connected_nodes[node_id]
|
hub/models.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
from pydantic import BaseModel, Field
|
| 2 |
-
from typing import Optional
|
| 3 |
-
from datetime import datetime
|
| 4 |
|
| 5 |
class NodeRegistration(BaseModel):
|
| 6 |
pubkey: str = Field(..., description="Node's public key or UUID")
|
|
|
|
| 1 |
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional
|
|
|
|
| 3 |
|
| 4 |
class NodeRegistration(BaseModel):
|
| 5 |
pubkey: str = Field(..., description="Node's public key or UUID")
|
hub/requirements.txt
CHANGED
|
@@ -2,4 +2,5 @@ fastapi
|
|
| 2 |
uvicorn
|
| 3 |
pydantic
|
| 4 |
websockets
|
| 5 |
-
cryptography
|
|
|
|
|
|
| 2 |
uvicorn
|
| 3 |
pydantic
|
| 4 |
websockets
|
| 5 |
+
cryptography
|
| 6 |
+
psycopg2-binary
|
node/client.py
CHANGED
|
@@ -3,27 +3,30 @@ import json
|
|
| 3 |
import websockets
|
| 4 |
import requests
|
| 5 |
import uuid
|
| 6 |
-
import
|
|
|
|
| 7 |
from reputation import ReputationManager
|
|
|
|
| 8 |
|
| 9 |
class ChronosNode:
|
| 10 |
"""
|
| 11 |
Simulated Clawdbot Client (Both Consumer & Provider)
|
| 12 |
"""
|
| 13 |
-
def __init__(self,
|
| 14 |
-
self.
|
|
|
|
| 15 |
self.hub_url = hub_url
|
| 16 |
self.ws_url = ws_url
|
| 17 |
-
self.reputation = ReputationManager(storage_path=f"reputation_{node_id}.json")
|
| 18 |
self.is_sleeping = False
|
| 19 |
|
| 20 |
# Track pending tasks we created (Consumer)
|
| 21 |
-
self.my_pending_tasks = {}
|
| 22 |
|
| 23 |
def register(self):
|
| 24 |
"""Register to get 10 SECONDS."""
|
| 25 |
print(f"[Node {self.node_id}] Registering with Hub...")
|
| 26 |
-
resp = requests.post(f"{self.hub_url}/register", json={"pubkey": self.
|
| 27 |
data = resp.json()
|
| 28 |
print(f"[Node {self.node_id}] Balance: {data['balance']}s")
|
| 29 |
|
|
@@ -35,8 +38,6 @@ class ChronosNode:
|
|
| 35 |
task_id = task_data["id"]
|
| 36 |
payload = task_data["payload"]
|
| 37 |
bounty = task_data["bounty"]
|
| 38 |
-
consumer_id = task_data["consumer_id"]
|
| 39 |
-
|
| 40 |
print(f"[Node {self.node_id}] Broadcast received: Task {task_id[:6]} for {bounty}s")
|
| 41 |
|
| 42 |
# 1. Check L2 Reputation of Consumer (Don't work for bad nodes)
|
|
@@ -47,11 +48,14 @@ class ChronosNode:
|
|
| 47 |
result = f"Hello from {self.node_id}. I processed your payload: {payload[:20]}..."
|
| 48 |
|
| 49 |
# 3. Submit proof of work
|
| 50 |
-
|
| 51 |
"task_id": task_id,
|
| 52 |
"provider_id": self.node_id,
|
| 53 |
"result_payload": result
|
| 54 |
})
|
|
|
|
|
|
|
|
|
|
| 55 |
if resp.status_code == 200:
|
| 56 |
print(f"[Node {self.node_id}] Mined {bounty}s! New Balance: {resp.json()['new_balance']}s")
|
| 57 |
|
|
@@ -71,7 +75,10 @@ class ChronosNode:
|
|
| 71 |
|
| 72 |
async def listen(self):
|
| 73 |
"""Persistent WebSocket connection."""
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
| 75 |
async with websockets.connect(uri) as ws:
|
| 76 |
print(f"[Node {self.node_id}] Connected to Hub via WebSocket.")
|
| 77 |
while True:
|
|
@@ -85,11 +92,14 @@ class ChronosNode:
|
|
| 85 |
|
| 86 |
async def submit_task(self, payload: str, bounty: float):
|
| 87 |
"""As a Consumer, create a task and lock SECONDS."""
|
| 88 |
-
|
| 89 |
"consumer_id": self.node_id,
|
| 90 |
"payload": payload,
|
| 91 |
"bounty": bounty
|
| 92 |
})
|
|
|
|
|
|
|
|
|
|
| 93 |
if resp.status_code == 200:
|
| 94 |
task_id = resp.json()["task_id"]
|
| 95 |
print(f"[Node {self.node_id} (Consumer)] Submitted Task {task_id[:6]} for {bounty}s")
|
|
@@ -98,11 +108,11 @@ class ChronosNode:
|
|
| 98 |
|
| 99 |
async def run_demo():
|
| 100 |
# Setup two nodes
|
| 101 |
-
usa_node = ChronosNode("
|
| 102 |
usa_node.is_sleeping = False
|
| 103 |
usa_node.register()
|
| 104 |
|
| 105 |
-
asia_node = ChronosNode("
|
| 106 |
asia_node.is_sleeping = True # Asia goes to sleep and mines
|
| 107 |
asia_node.register()
|
| 108 |
|
|
|
|
| 3 |
import websockets
|
| 4 |
import requests
|
| 5 |
import uuid
|
| 6 |
+
import time
|
| 7 |
+
import urllib.parse
|
| 8 |
from reputation import ReputationManager
|
| 9 |
+
from identity import MEPIdentity
|
| 10 |
|
| 11 |
class ChronosNode:
|
| 12 |
"""
|
| 13 |
Simulated Clawdbot Client (Both Consumer & Provider)
|
| 14 |
"""
|
| 15 |
+
def __init__(self, key_path: str, hub_url: str = "http://localhost:8000", ws_url: str = "ws://localhost:8000"):
|
| 16 |
+
self.identity = MEPIdentity(key_path)
|
| 17 |
+
self.node_id = self.identity.node_id
|
| 18 |
self.hub_url = hub_url
|
| 19 |
self.ws_url = ws_url
|
| 20 |
+
self.reputation = ReputationManager(storage_path=f"reputation_{self.node_id}.json")
|
| 21 |
self.is_sleeping = False
|
| 22 |
|
| 23 |
# Track pending tasks we created (Consumer)
|
| 24 |
+
self.my_pending_tasks: dict[str, dict] = {}
|
| 25 |
|
| 26 |
def register(self):
|
| 27 |
"""Register to get 10 SECONDS."""
|
| 28 |
print(f"[Node {self.node_id}] Registering with Hub...")
|
| 29 |
+
resp = requests.post(f"{self.hub_url}/register", json={"pubkey": self.identity.pub_pem, "alias": "test"})
|
| 30 |
data = resp.json()
|
| 31 |
print(f"[Node {self.node_id}] Balance: {data['balance']}s")
|
| 32 |
|
|
|
|
| 38 |
task_id = task_data["id"]
|
| 39 |
payload = task_data["payload"]
|
| 40 |
bounty = task_data["bounty"]
|
|
|
|
|
|
|
| 41 |
print(f"[Node {self.node_id}] Broadcast received: Task {task_id[:6]} for {bounty}s")
|
| 42 |
|
| 43 |
# 1. Check L2 Reputation of Consumer (Don't work for bad nodes)
|
|
|
|
| 48 |
result = f"Hello from {self.node_id}. I processed your payload: {payload[:20]}..."
|
| 49 |
|
| 50 |
# 3. Submit proof of work
|
| 51 |
+
payload_str = json.dumps({
|
| 52 |
"task_id": task_id,
|
| 53 |
"provider_id": self.node_id,
|
| 54 |
"result_payload": result
|
| 55 |
})
|
| 56 |
+
headers = self.identity.get_auth_headers(payload_str)
|
| 57 |
+
headers["Content-Type"] = "application/json"
|
| 58 |
+
resp = requests.post(f"{self.hub_url}/tasks/complete", data=payload_str, headers=headers)
|
| 59 |
if resp.status_code == 200:
|
| 60 |
print(f"[Node {self.node_id}] Mined {bounty}s! New Balance: {resp.json()['new_balance']}s")
|
| 61 |
|
|
|
|
| 75 |
|
| 76 |
async def listen(self):
|
| 77 |
"""Persistent WebSocket connection."""
|
| 78 |
+
ts = str(int(time.time()))
|
| 79 |
+
sig = self.identity.sign(self.node_id, ts)
|
| 80 |
+
sig_safe = urllib.parse.quote(sig)
|
| 81 |
+
uri = f"{self.ws_url}/ws/{self.node_id}?timestamp={ts}&signature={sig_safe}"
|
| 82 |
async with websockets.connect(uri) as ws:
|
| 83 |
print(f"[Node {self.node_id}] Connected to Hub via WebSocket.")
|
| 84 |
while True:
|
|
|
|
| 92 |
|
| 93 |
async def submit_task(self, payload: str, bounty: float):
|
| 94 |
"""As a Consumer, create a task and lock SECONDS."""
|
| 95 |
+
payload_str = json.dumps({
|
| 96 |
"consumer_id": self.node_id,
|
| 97 |
"payload": payload,
|
| 98 |
"bounty": bounty
|
| 99 |
})
|
| 100 |
+
headers = self.identity.get_auth_headers(payload_str)
|
| 101 |
+
headers["Content-Type"] = "application/json"
|
| 102 |
+
resp = requests.post(f"{self.hub_url}/tasks/submit", data=payload_str, headers=headers)
|
| 103 |
if resp.status_code == 200:
|
| 104 |
task_id = resp.json()["task_id"]
|
| 105 |
print(f"[Node {self.node_id} (Consumer)] Submitted Task {task_id[:6]} for {bounty}s")
|
|
|
|
| 108 |
|
| 109 |
async def run_demo():
|
| 110 |
# Setup two nodes
|
| 111 |
+
usa_node = ChronosNode(f"usa_node_{uuid.uuid4().hex[:6]}.pem")
|
| 112 |
usa_node.is_sleeping = False
|
| 113 |
usa_node.register()
|
| 114 |
|
| 115 |
+
asia_node = ChronosNode(f"asia_node_{uuid.uuid4().hex[:6]}.pem")
|
| 116 |
asia_node.is_sleeping = True # Asia goes to sleep and mines
|
| 117 |
asia_node.register()
|
| 118 |
|
node/mep_cli_provider.py
CHANGED
|
@@ -9,22 +9,25 @@ import websockets
|
|
| 9 |
import json
|
| 10 |
import requests
|
| 11 |
import uuid
|
| 12 |
-
import sys
|
| 13 |
import os
|
| 14 |
import shlex
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
HUB_URL = "http://localhost:8000"
|
| 17 |
WS_URL = "ws://localhost:8000"
|
| 18 |
|
| 19 |
class MEPCLIProvider:
|
| 20 |
-
def __init__(self,
|
| 21 |
-
self.
|
|
|
|
| 22 |
self.balance = 0.0
|
| 23 |
self.is_contributing = True
|
| 24 |
self.capabilities = ["cli-agent", "bash", "python"]
|
| 25 |
|
| 26 |
-
|
| 27 |
-
self.workspace_dir = "/tmp/mep_workspaces"
|
| 28 |
os.makedirs(self.workspace_dir, exist_ok=True)
|
| 29 |
|
| 30 |
async def connect(self):
|
|
@@ -33,17 +36,20 @@ class MEPCLIProvider:
|
|
| 33 |
|
| 34 |
# Register with hub
|
| 35 |
try:
|
| 36 |
-
resp =
|
| 37 |
self.balance = resp.json().get("balance", 0.0)
|
| 38 |
print(f"[CLI Provider] Registered. Balance: {self.balance:.6f} SECONDS")
|
| 39 |
except Exception as e:
|
| 40 |
print(f"[CLI Provider] Registration failed: {e}")
|
| 41 |
return
|
| 42 |
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
try:
|
| 45 |
async with websockets.connect(uri) as ws:
|
| 46 |
-
print(
|
| 47 |
while self.is_contributing:
|
| 48 |
try:
|
| 49 |
msg = await asyncio.wait_for(ws.recv(), timeout=1.0)
|
|
@@ -75,10 +81,13 @@ class MEPCLIProvider:
|
|
| 75 |
print(f"[CLI Provider] Received matching RFC {task_id[:8]} for {bounty:.6f} SECONDS. Bidding...")
|
| 76 |
|
| 77 |
try:
|
| 78 |
-
|
| 79 |
"task_id": task_id,
|
| 80 |
"provider_id": self.node_id
|
| 81 |
})
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
if resp.status_code == 200:
|
| 84 |
data = resp.json()
|
|
@@ -136,11 +145,14 @@ class MEPCLIProvider:
|
|
| 136 |
result_payload = f"```bash\n{output}\n```\n*Workspace: {task_dir}*"
|
| 137 |
|
| 138 |
# Submit result back to Hub
|
| 139 |
-
|
| 140 |
"task_id": task_id,
|
| 141 |
"provider_id": self.node_id,
|
| 142 |
"result_payload": result_payload
|
| 143 |
})
|
|
|
|
|
|
|
|
|
|
| 144 |
print(f"[CLI Provider] Result submitted! Earned {bounty:.6f} SECONDS.\n")
|
| 145 |
|
| 146 |
if __name__ == "__main__":
|
|
@@ -149,8 +161,8 @@ if __name__ == "__main__":
|
|
| 149 |
print("WARNING: This node executes shell commands. Use sandboxing!")
|
| 150 |
print("=" * 60)
|
| 151 |
|
| 152 |
-
|
| 153 |
-
provider = MEPCLIProvider(
|
| 154 |
|
| 155 |
try:
|
| 156 |
asyncio.run(provider.connect())
|
|
|
|
| 9 |
import json
|
| 10 |
import requests
|
| 11 |
import uuid
|
|
|
|
| 12 |
import os
|
| 13 |
import shlex
|
| 14 |
+
import time
|
| 15 |
+
import urllib.parse
|
| 16 |
+
import tempfile
|
| 17 |
+
from identity import MEPIdentity
|
| 18 |
|
| 19 |
HUB_URL = "http://localhost:8000"
|
| 20 |
WS_URL = "ws://localhost:8000"
|
| 21 |
|
| 22 |
class MEPCLIProvider:
|
| 23 |
+
def __init__(self, key_path: str):
|
| 24 |
+
self.identity = MEPIdentity(key_path)
|
| 25 |
+
self.node_id = self.identity.node_id
|
| 26 |
self.balance = 0.0
|
| 27 |
self.is_contributing = True
|
| 28 |
self.capabilities = ["cli-agent", "bash", "python"]
|
| 29 |
|
| 30 |
+
self.workspace_dir = os.path.join(tempfile.gettempdir(), "mep_workspaces")
|
|
|
|
| 31 |
os.makedirs(self.workspace_dir, exist_ok=True)
|
| 32 |
|
| 33 |
async def connect(self):
|
|
|
|
| 36 |
|
| 37 |
# Register with hub
|
| 38 |
try:
|
| 39 |
+
resp = requests.post(f"{HUB_URL}/register", json={"pubkey": self.identity.pub_pem})
|
| 40 |
self.balance = resp.json().get("balance", 0.0)
|
| 41 |
print(f"[CLI Provider] Registered. Balance: {self.balance:.6f} SECONDS")
|
| 42 |
except Exception as e:
|
| 43 |
print(f"[CLI Provider] Registration failed: {e}")
|
| 44 |
return
|
| 45 |
|
| 46 |
+
ts = str(int(time.time()))
|
| 47 |
+
sig = self.identity.sign(self.node_id, ts)
|
| 48 |
+
sig_safe = urllib.parse.quote(sig)
|
| 49 |
+
uri = f"{WS_URL}/ws/{self.node_id}?timestamp={ts}&signature={sig_safe}"
|
| 50 |
try:
|
| 51 |
async with websockets.connect(uri) as ws:
|
| 52 |
+
print("[CLI Provider] Connected to MEP Hub. Awaiting CLI tasks...")
|
| 53 |
while self.is_contributing:
|
| 54 |
try:
|
| 55 |
msg = await asyncio.wait_for(ws.recv(), timeout=1.0)
|
|
|
|
| 81 |
print(f"[CLI Provider] Received matching RFC {task_id[:8]} for {bounty:.6f} SECONDS. Bidding...")
|
| 82 |
|
| 83 |
try:
|
| 84 |
+
payload_str = json.dumps({
|
| 85 |
"task_id": task_id,
|
| 86 |
"provider_id": self.node_id
|
| 87 |
})
|
| 88 |
+
headers = self.identity.get_auth_headers(payload_str)
|
| 89 |
+
headers["Content-Type"] = "application/json"
|
| 90 |
+
resp = requests.post(f"{HUB_URL}/tasks/bid", data=payload_str, headers=headers)
|
| 91 |
|
| 92 |
if resp.status_code == 200:
|
| 93 |
data = resp.json()
|
|
|
|
| 145 |
result_payload = f"```bash\n{output}\n```\n*Workspace: {task_dir}*"
|
| 146 |
|
| 147 |
# Submit result back to Hub
|
| 148 |
+
payload_str = json.dumps({
|
| 149 |
"task_id": task_id,
|
| 150 |
"provider_id": self.node_id,
|
| 151 |
"result_payload": result_payload
|
| 152 |
})
|
| 153 |
+
headers = self.identity.get_auth_headers(payload_str)
|
| 154 |
+
headers["Content-Type"] = "application/json"
|
| 155 |
+
requests.post(f"{HUB_URL}/tasks/complete", data=payload_str, headers=headers)
|
| 156 |
print(f"[CLI Provider] Result submitted! Earned {bounty:.6f} SECONDS.\n")
|
| 157 |
|
| 158 |
if __name__ == "__main__":
|
|
|
|
| 161 |
print("WARNING: This node executes shell commands. Use sandboxing!")
|
| 162 |
print("=" * 60)
|
| 163 |
|
| 164 |
+
key_path = f"cli_provider_{uuid.uuid4().hex[:6]}.pem"
|
| 165 |
+
provider = MEPCLIProvider(key_path)
|
| 166 |
|
| 167 |
try:
|
| 168 |
asyncio.run(provider.connect())
|
node/mep_provider.py
CHANGED
|
@@ -10,16 +10,20 @@ import requests
|
|
| 10 |
import uuid
|
| 11 |
import sys
|
| 12 |
import os
|
|
|
|
|
|
|
| 13 |
|
| 14 |
# Add parent directory to path for imports
|
| 15 |
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
| 16 |
|
| 17 |
HUB_URL = "http://localhost:8000"
|
| 18 |
WS_URL = "ws://localhost:8000"
|
| 19 |
|
| 20 |
class MEPProvider:
|
| 21 |
-
def __init__(self,
|
| 22 |
-
self.
|
|
|
|
| 23 |
self.balance = 0.0
|
| 24 |
self.is_mining = True
|
| 25 |
|
|
@@ -29,7 +33,7 @@ class MEPProvider:
|
|
| 29 |
|
| 30 |
# Register with hub
|
| 31 |
try:
|
| 32 |
-
resp =
|
| 33 |
data = resp.json()
|
| 34 |
self.balance = data.get("balance", 0.0)
|
| 35 |
print(f"[MEP Provider {self.node_id}] Registered. Balance: {self.balance:.6f} SECONDS")
|
|
@@ -38,7 +42,10 @@ class MEPProvider:
|
|
| 38 |
return
|
| 39 |
|
| 40 |
# Connect to WebSocket
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
| 42 |
try:
|
| 43 |
async with websockets.connect(uri) as ws:
|
| 44 |
print(f"[MEP Provider {self.node_id}] Connected to MEP Hub")
|
|
@@ -67,8 +74,6 @@ class MEPProvider:
|
|
| 67 |
"""Phase 2: Evaluate Request For Compute and submit Bid."""
|
| 68 |
task_id = rfc_data["id"]
|
| 69 |
bounty = rfc_data["bounty"]
|
| 70 |
-
model = rfc_data.get("model_requirement")
|
| 71 |
-
|
| 72 |
# SAFETY SWITCH: Prevent purchasing data unless explicitly allowed
|
| 73 |
max_purchase_price = 0.0 # Set to e.g., -5.0 to buy premium data
|
| 74 |
if bounty < max_purchase_price:
|
|
@@ -79,10 +84,13 @@ class MEPProvider:
|
|
| 79 |
|
| 80 |
# Place bid
|
| 81 |
try:
|
| 82 |
-
|
| 83 |
"task_id": task_id,
|
| 84 |
"provider_id": self.node_id
|
| 85 |
})
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
if resp.status_code == 200:
|
| 88 |
data = resp.json()
|
|
@@ -107,8 +115,6 @@ class MEPProvider:
|
|
| 107 |
task_id = task_data["id"]
|
| 108 |
payload = task_data["payload"]
|
| 109 |
bounty = task_data["bounty"]
|
| 110 |
-
consumer_id = task_data["consumer_id"]
|
| 111 |
-
|
| 112 |
print(f"[MEP Provider {self.node_id}] Received task {task_id[:8]} for {bounty:.6f} SECONDS")
|
| 113 |
print(f" Payload: {payload[:50]}...")
|
| 114 |
|
|
@@ -131,11 +137,14 @@ Would you like me to elaborate on any specific aspect?"""
|
|
| 131 |
|
| 132 |
# Submit result
|
| 133 |
try:
|
| 134 |
-
|
| 135 |
"task_id": task_id,
|
| 136 |
"provider_id": self.node_id,
|
| 137 |
"result_payload": result
|
| 138 |
})
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
if resp.status_code == 200:
|
| 141 |
data = resp.json()
|
|
@@ -154,9 +163,8 @@ Would you like me to elaborate on any specific aspect?"""
|
|
| 154 |
print(f"[MEP Provider {self.node_id}] Stopping...")
|
| 155 |
|
| 156 |
async def main():
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
miner = MEPProvider(provider_id)
|
| 160 |
|
| 161 |
try:
|
| 162 |
await miner.connect()
|
|
|
|
| 10 |
import uuid
|
| 11 |
import sys
|
| 12 |
import os
|
| 13 |
+
import time
|
| 14 |
+
import urllib.parse
|
| 15 |
|
| 16 |
# Add parent directory to path for imports
|
| 17 |
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 18 |
+
from identity import MEPIdentity
|
| 19 |
|
| 20 |
HUB_URL = "http://localhost:8000"
|
| 21 |
WS_URL = "ws://localhost:8000"
|
| 22 |
|
| 23 |
class MEPProvider:
|
| 24 |
+
def __init__(self, key_path: str):
|
| 25 |
+
self.identity = MEPIdentity(key_path)
|
| 26 |
+
self.node_id = self.identity.node_id
|
| 27 |
self.balance = 0.0
|
| 28 |
self.is_mining = True
|
| 29 |
|
|
|
|
| 33 |
|
| 34 |
# Register with hub
|
| 35 |
try:
|
| 36 |
+
resp = requests.post(f"{HUB_URL}/register", json={"pubkey": self.identity.pub_pem})
|
| 37 |
data = resp.json()
|
| 38 |
self.balance = data.get("balance", 0.0)
|
| 39 |
print(f"[MEP Provider {self.node_id}] Registered. Balance: {self.balance:.6f} SECONDS")
|
|
|
|
| 42 |
return
|
| 43 |
|
| 44 |
# Connect to WebSocket
|
| 45 |
+
ts = str(int(time.time()))
|
| 46 |
+
sig = self.identity.sign(self.node_id, ts)
|
| 47 |
+
sig_safe = urllib.parse.quote(sig)
|
| 48 |
+
uri = f"{WS_URL}/ws/{self.node_id}?timestamp={ts}&signature={sig_safe}"
|
| 49 |
try:
|
| 50 |
async with websockets.connect(uri) as ws:
|
| 51 |
print(f"[MEP Provider {self.node_id}] Connected to MEP Hub")
|
|
|
|
| 74 |
"""Phase 2: Evaluate Request For Compute and submit Bid."""
|
| 75 |
task_id = rfc_data["id"]
|
| 76 |
bounty = rfc_data["bounty"]
|
|
|
|
|
|
|
| 77 |
# SAFETY SWITCH: Prevent purchasing data unless explicitly allowed
|
| 78 |
max_purchase_price = 0.0 # Set to e.g., -5.0 to buy premium data
|
| 79 |
if bounty < max_purchase_price:
|
|
|
|
| 84 |
|
| 85 |
# Place bid
|
| 86 |
try:
|
| 87 |
+
payload_str = json.dumps({
|
| 88 |
"task_id": task_id,
|
| 89 |
"provider_id": self.node_id
|
| 90 |
})
|
| 91 |
+
headers = self.identity.get_auth_headers(payload_str)
|
| 92 |
+
headers["Content-Type"] = "application/json"
|
| 93 |
+
resp = requests.post(f"{HUB_URL}/tasks/bid", data=payload_str, headers=headers)
|
| 94 |
|
| 95 |
if resp.status_code == 200:
|
| 96 |
data = resp.json()
|
|
|
|
| 115 |
task_id = task_data["id"]
|
| 116 |
payload = task_data["payload"]
|
| 117 |
bounty = task_data["bounty"]
|
|
|
|
|
|
|
| 118 |
print(f"[MEP Provider {self.node_id}] Received task {task_id[:8]} for {bounty:.6f} SECONDS")
|
| 119 |
print(f" Payload: {payload[:50]}...")
|
| 120 |
|
|
|
|
| 137 |
|
| 138 |
# Submit result
|
| 139 |
try:
|
| 140 |
+
payload_str = json.dumps({
|
| 141 |
"task_id": task_id,
|
| 142 |
"provider_id": self.node_id,
|
| 143 |
"result_payload": result
|
| 144 |
})
|
| 145 |
+
headers = self.identity.get_auth_headers(payload_str)
|
| 146 |
+
headers["Content-Type"] = "application/json"
|
| 147 |
+
resp = requests.post(f"{HUB_URL}/tasks/complete", data=payload_str, headers=headers)
|
| 148 |
|
| 149 |
if resp.status_code == 200:
|
| 150 |
data = resp.json()
|
|
|
|
| 163 |
print(f"[MEP Provider {self.node_id}] Stopping...")
|
| 164 |
|
| 165 |
async def main():
|
| 166 |
+
key_path = f"mep_provider_{uuid.uuid4().hex[:8]}.pem"
|
| 167 |
+
miner = MEPProvider(key_path)
|
|
|
|
| 168 |
|
| 169 |
try:
|
| 170 |
await miner.connect()
|
node/race_test.py
CHANGED
|
@@ -9,6 +9,8 @@ import json
|
|
| 9 |
import requests
|
| 10 |
import uuid
|
| 11 |
import time
|
|
|
|
|
|
|
| 12 |
|
| 13 |
HUB_URL = "http://localhost:8000"
|
| 14 |
WS_URL = "ws://localhost:8000"
|
|
@@ -17,7 +19,8 @@ class RacingProvider:
|
|
| 17 |
def __init__(self, name, location):
|
| 18 |
self.name = name
|
| 19 |
self.location = location
|
| 20 |
-
self.
|
|
|
|
| 21 |
self.balance = 0
|
| 22 |
self.won_race = False
|
| 23 |
self.response_time = None
|
|
@@ -27,11 +30,14 @@ class RacingProvider:
|
|
| 27 |
print(f"[{self.name} in {self.location}] Connecting to MEP Hub...")
|
| 28 |
|
| 29 |
# Register
|
| 30 |
-
requests.post(f"{HUB_URL}/register", json={"pubkey": self.
|
| 31 |
|
| 32 |
# Connect via WebSocket
|
| 33 |
start_time = time.time()
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
print(f"[{self.name}] Connected. Waiting for task {task_id[:8]}...")
|
| 36 |
|
| 37 |
try:
|
|
@@ -49,11 +55,14 @@ class RacingProvider:
|
|
| 49 |
|
| 50 |
# Submit result
|
| 51 |
result = f"Processed by {self.name} from {self.location}. Task: {task_payload[:30]}..."
|
| 52 |
-
|
| 53 |
"task_id": task_id,
|
| 54 |
"provider_id": self.node_id,
|
| 55 |
"result_payload": result
|
| 56 |
})
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
if resp.status_code == 200:
|
| 59 |
self.won_race = True
|
|
@@ -73,8 +82,8 @@ async def run_race():
|
|
| 73 |
print("=" * 60)
|
| 74 |
|
| 75 |
# Register consumer
|
| 76 |
-
|
| 77 |
-
requests.post(f"{HUB_URL}/register", json={"pubkey":
|
| 78 |
|
| 79 |
# Create 4 providers in different "locations"
|
| 80 |
providers = [
|
|
@@ -91,11 +100,14 @@ async def run_race():
|
|
| 91 |
print(f"\nπ€ Submitting task: {task_payload[:50]}...")
|
| 92 |
print(f" Bounty: {bounty} SECONDS")
|
| 93 |
|
| 94 |
-
|
| 95 |
-
"consumer_id":
|
| 96 |
"payload": task_payload,
|
| 97 |
"bounty": bounty
|
| 98 |
})
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
task_data = resp.json()
|
| 101 |
task_id = task_data["task_id"]
|
|
@@ -128,7 +140,7 @@ async def run_race():
|
|
| 128 |
print("\nβ οΈ No winner - task may have failed")
|
| 129 |
|
| 130 |
# Check consumer balance
|
| 131 |
-
balance_resp = requests.get(f"{HUB_URL}/balance/{
|
| 132 |
consumer_balance = balance_resp.json()["balance_seconds"]
|
| 133 |
print(f"\nπ° Consumer spent {bounty} SECONDS, new balance: {consumer_balance}")
|
| 134 |
|
|
|
|
| 9 |
import requests
|
| 10 |
import uuid
|
| 11 |
import time
|
| 12 |
+
import urllib.parse
|
| 13 |
+
from identity import MEPIdentity
|
| 14 |
|
| 15 |
HUB_URL = "http://localhost:8000"
|
| 16 |
WS_URL = "ws://localhost:8000"
|
|
|
|
| 19 |
def __init__(self, name, location):
|
| 20 |
self.name = name
|
| 21 |
self.location = location
|
| 22 |
+
self.identity = MEPIdentity(f"{name.replace(' ', '_')}_{uuid.uuid4().hex[:6]}.pem")
|
| 23 |
+
self.node_id = self.identity.node_id
|
| 24 |
self.balance = 0
|
| 25 |
self.won_race = False
|
| 26 |
self.response_time = None
|
|
|
|
| 30 |
print(f"[{self.name} in {self.location}] Connecting to MEP Hub...")
|
| 31 |
|
| 32 |
# Register
|
| 33 |
+
requests.post(f"{HUB_URL}/register", json={"pubkey": self.identity.pub_pem})
|
| 34 |
|
| 35 |
# Connect via WebSocket
|
| 36 |
start_time = time.time()
|
| 37 |
+
ts = str(int(time.time()))
|
| 38 |
+
sig = self.identity.sign(self.node_id, ts)
|
| 39 |
+
sig_safe = urllib.parse.quote(sig)
|
| 40 |
+
async with websockets.connect(f"{WS_URL}/ws/{self.node_id}?timestamp={ts}&signature={sig_safe}") as ws:
|
| 41 |
print(f"[{self.name}] Connected. Waiting for task {task_id[:8]}...")
|
| 42 |
|
| 43 |
try:
|
|
|
|
| 55 |
|
| 56 |
# Submit result
|
| 57 |
result = f"Processed by {self.name} from {self.location}. Task: {task_payload[:30]}..."
|
| 58 |
+
payload_str = json.dumps({
|
| 59 |
"task_id": task_id,
|
| 60 |
"provider_id": self.node_id,
|
| 61 |
"result_payload": result
|
| 62 |
})
|
| 63 |
+
headers = self.identity.get_auth_headers(payload_str)
|
| 64 |
+
headers["Content-Type"] = "application/json"
|
| 65 |
+
resp = requests.post(f"{HUB_URL}/tasks/complete", data=payload_str, headers=headers)
|
| 66 |
|
| 67 |
if resp.status_code == 200:
|
| 68 |
self.won_race = True
|
|
|
|
| 82 |
print("=" * 60)
|
| 83 |
|
| 84 |
# Register consumer
|
| 85 |
+
consumer = MEPIdentity(f"race_consumer_{uuid.uuid4().hex[:6]}.pem")
|
| 86 |
+
requests.post(f"{HUB_URL}/register", json={"pubkey": consumer.pub_pem})
|
| 87 |
|
| 88 |
# Create 4 providers in different "locations"
|
| 89 |
providers = [
|
|
|
|
| 100 |
print(f"\nπ€ Submitting task: {task_payload[:50]}...")
|
| 101 |
print(f" Bounty: {bounty} SECONDS")
|
| 102 |
|
| 103 |
+
payload_str = json.dumps({
|
| 104 |
+
"consumer_id": consumer.node_id,
|
| 105 |
"payload": task_payload,
|
| 106 |
"bounty": bounty
|
| 107 |
})
|
| 108 |
+
headers = consumer.get_auth_headers(payload_str)
|
| 109 |
+
headers["Content-Type"] = "application/json"
|
| 110 |
+
resp = requests.post(f"{HUB_URL}/tasks/submit", data=payload_str, headers=headers)
|
| 111 |
|
| 112 |
task_data = resp.json()
|
| 113 |
task_id = task_data["task_id"]
|
|
|
|
| 140 |
print("\nβ οΈ No winner - task may have failed")
|
| 141 |
|
| 142 |
# Check consumer balance
|
| 143 |
+
balance_resp = requests.get(f"{HUB_URL}/balance/{consumer.node_id}")
|
| 144 |
consumer_balance = balance_resp.json()["balance_seconds"]
|
| 145 |
print(f"\nπ° Consumer spent {bounty} SECONDS, new balance: {consumer_balance}")
|
| 146 |
|
node/race_test_fixed.py
CHANGED
|
@@ -8,6 +8,8 @@ import json
|
|
| 8 |
import requests
|
| 9 |
import uuid
|
| 10 |
import time
|
|
|
|
|
|
|
| 11 |
|
| 12 |
HUB_URL = "http://localhost:8000"
|
| 13 |
WS_URL = "ws://localhost:8000"
|
|
@@ -16,7 +18,8 @@ class RacingProvider:
|
|
| 16 |
def __init__(self, name, location):
|
| 17 |
self.name = name
|
| 18 |
self.location = location
|
| 19 |
-
self.
|
|
|
|
| 20 |
self.balance = 0
|
| 21 |
self.won_race = False
|
| 22 |
self.response_time = None
|
|
@@ -24,8 +27,11 @@ class RacingProvider:
|
|
| 24 |
|
| 25 |
async def connect(self):
|
| 26 |
"""Connect to hub and wait for tasks."""
|
| 27 |
-
requests.post(f"{HUB_URL}/register", json={"pubkey": self.
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 29 |
print(f"[{self.name}] Connected to hub")
|
| 30 |
return self.ws
|
| 31 |
|
|
@@ -44,11 +50,14 @@ class RacingProvider:
|
|
| 44 |
await asyncio.sleep(0.05) # Very fast!
|
| 45 |
|
| 46 |
result = f"WON by {self.name} from {self.location}. Response time: {self.response_time:.3f}s"
|
| 47 |
-
|
| 48 |
"task_id": task_id,
|
| 49 |
"provider_id": self.node_id,
|
| 50 |
"result_payload": result
|
| 51 |
})
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
if resp.status_code == 200:
|
| 54 |
self.won_race = True
|
|
@@ -87,8 +96,8 @@ async def run_race():
|
|
| 87 |
await asyncio.sleep(0.5) # Ensure all connected
|
| 88 |
|
| 89 |
# Register consumer and submit task
|
| 90 |
-
|
| 91 |
-
requests.post(f"{HUB_URL}/register", json={"pubkey":
|
| 92 |
|
| 93 |
task_payload = "Which provider is fastest in the MEP race?"
|
| 94 |
bounty = 7.5
|
|
@@ -97,18 +106,21 @@ async def run_race():
|
|
| 97 |
print(f" Task: {task_payload}")
|
| 98 |
print(f" Bounty: {bounty} SECONDS")
|
| 99 |
|
| 100 |
-
|
| 101 |
-
"consumer_id":
|
| 102 |
"payload": task_payload,
|
| 103 |
"bounty": bounty
|
| 104 |
})
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
task_id = resp.json()["task_id"]
|
| 107 |
print(f" Task ID: {task_id[:8]}...")
|
| 108 |
|
| 109 |
# All providers listen simultaneously
|
| 110 |
print("\nπ ALL PROVIDERS LISTENING... RACE STARTS!")
|
| 111 |
-
|
| 112 |
|
| 113 |
# Close connections
|
| 114 |
for provider in providers:
|
|
@@ -129,7 +141,7 @@ async def run_race():
|
|
| 129 |
print(f" New balance: {winner.balance} SECONDS")
|
| 130 |
|
| 131 |
# Show all times
|
| 132 |
-
print(
|
| 133 |
for provider in providers:
|
| 134 |
if provider.response_time:
|
| 135 |
status = "β
WON" if provider.won_race else "β Lost"
|
|
@@ -138,7 +150,7 @@ async def run_race():
|
|
| 138 |
print("β No winner - check hub logs")
|
| 139 |
|
| 140 |
# Consumer balance
|
| 141 |
-
balance_resp = requests.get(f"{HUB_URL}/balance/{
|
| 142 |
consumer_balance = balance_resp.json()["balance_seconds"]
|
| 143 |
print(f"\nπ° Consumer balance: {consumer_balance} SECONDS")
|
| 144 |
|
|
|
|
| 8 |
import requests
|
| 9 |
import uuid
|
| 10 |
import time
|
| 11 |
+
import urllib.parse
|
| 12 |
+
from identity import MEPIdentity
|
| 13 |
|
| 14 |
HUB_URL = "http://localhost:8000"
|
| 15 |
WS_URL = "ws://localhost:8000"
|
|
|
|
| 18 |
def __init__(self, name, location):
|
| 19 |
self.name = name
|
| 20 |
self.location = location
|
| 21 |
+
self.identity = MEPIdentity(f"{name.replace(' ', '_')}_{uuid.uuid4().hex[:6]}.pem")
|
| 22 |
+
self.node_id = self.identity.node_id
|
| 23 |
self.balance = 0
|
| 24 |
self.won_race = False
|
| 25 |
self.response_time = None
|
|
|
|
| 27 |
|
| 28 |
async def connect(self):
|
| 29 |
"""Connect to hub and wait for tasks."""
|
| 30 |
+
requests.post(f"{HUB_URL}/register", json={"pubkey": self.identity.pub_pem})
|
| 31 |
+
ts = str(int(time.time()))
|
| 32 |
+
sig = self.identity.sign(self.node_id, ts)
|
| 33 |
+
sig_safe = urllib.parse.quote(sig)
|
| 34 |
+
self.ws = await websockets.connect(f"{WS_URL}/ws/{self.node_id}?timestamp={ts}&signature={sig_safe}")
|
| 35 |
print(f"[{self.name}] Connected to hub")
|
| 36 |
return self.ws
|
| 37 |
|
|
|
|
| 50 |
await asyncio.sleep(0.05) # Very fast!
|
| 51 |
|
| 52 |
result = f"WON by {self.name} from {self.location}. Response time: {self.response_time:.3f}s"
|
| 53 |
+
payload_str = json.dumps({
|
| 54 |
"task_id": task_id,
|
| 55 |
"provider_id": self.node_id,
|
| 56 |
"result_payload": result
|
| 57 |
})
|
| 58 |
+
headers = self.identity.get_auth_headers(payload_str)
|
| 59 |
+
headers["Content-Type"] = "application/json"
|
| 60 |
+
resp = requests.post(f"{HUB_URL}/tasks/complete", data=payload_str, headers=headers)
|
| 61 |
|
| 62 |
if resp.status_code == 200:
|
| 63 |
self.won_race = True
|
|
|
|
| 96 |
await asyncio.sleep(0.5) # Ensure all connected
|
| 97 |
|
| 98 |
# Register consumer and submit task
|
| 99 |
+
consumer = MEPIdentity(f"race_consumer_{uuid.uuid4().hex[:6]}.pem")
|
| 100 |
+
requests.post(f"{HUB_URL}/register", json={"pubkey": consumer.pub_pem})
|
| 101 |
|
| 102 |
task_payload = "Which provider is fastest in the MEP race?"
|
| 103 |
bounty = 7.5
|
|
|
|
| 106 |
print(f" Task: {task_payload}")
|
| 107 |
print(f" Bounty: {bounty} SECONDS")
|
| 108 |
|
| 109 |
+
payload_str = json.dumps({
|
| 110 |
+
"consumer_id": consumer.node_id,
|
| 111 |
"payload": task_payload,
|
| 112 |
"bounty": bounty
|
| 113 |
})
|
| 114 |
+
headers = consumer.get_auth_headers(payload_str)
|
| 115 |
+
headers["Content-Type"] = "application/json"
|
| 116 |
+
resp = requests.post(f"{HUB_URL}/tasks/submit", data=payload_str, headers=headers)
|
| 117 |
|
| 118 |
task_id = resp.json()["task_id"]
|
| 119 |
print(f" Task ID: {task_id[:8]}...")
|
| 120 |
|
| 121 |
# All providers listen simultaneously
|
| 122 |
print("\nπ ALL PROVIDERS LISTENING... RACE STARTS!")
|
| 123 |
+
await asyncio.gather(*[provider.listen_for_task(task_id, bounty) for provider in providers])
|
| 124 |
|
| 125 |
# Close connections
|
| 126 |
for provider in providers:
|
|
|
|
| 141 |
print(f" New balance: {winner.balance} SECONDS")
|
| 142 |
|
| 143 |
# Show all times
|
| 144 |
+
print("\nπ All response times:")
|
| 145 |
for provider in providers:
|
| 146 |
if provider.response_time:
|
| 147 |
status = "β
WON" if provider.won_race else "β Lost"
|
|
|
|
| 150 |
print("β No winner - check hub logs")
|
| 151 |
|
| 152 |
# Consumer balance
|
| 153 |
+
balance_resp = requests.get(f"{HUB_URL}/balance/{consumer.node_id}")
|
| 154 |
consumer_balance = balance_resp.json()["balance_seconds"]
|
| 155 |
print(f"\nπ° Consumer balance: {consumer_balance} SECONDS")
|
| 156 |
|
node/test_auction.py
CHANGED
|
@@ -3,16 +3,25 @@ import websockets
|
|
| 3 |
import json
|
| 4 |
import requests
|
| 5 |
import uuid
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
HUB_URL = "http://localhost:8000"
|
| 8 |
|
| 9 |
async def test():
|
| 10 |
-
provider = f
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
msg = await asyncio.wait_for(ws.recv(), timeout=2.0)
|
| 18 |
data = json.loads(msg)
|
|
@@ -20,14 +29,20 @@ async def test():
|
|
| 20 |
|
| 21 |
if data['event'] == 'rfc':
|
| 22 |
task_id = data['data']['id']
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
print('Bid response:', resp.json())
|
| 25 |
|
| 26 |
-
|
| 27 |
-
'task_id': task_id,
|
| 28 |
-
'provider_id': provider,
|
| 29 |
'result_payload': 'Done!'
|
| 30 |
})
|
|
|
|
|
|
|
|
|
|
| 31 |
print('Complete response:', complete_resp.json())
|
| 32 |
|
| 33 |
if __name__ == '__main__':
|
|
|
|
| 3 |
import json
|
| 4 |
import requests
|
| 5 |
import uuid
|
| 6 |
+
import time
|
| 7 |
+
import urllib.parse
|
| 8 |
+
from identity import MEPIdentity
|
| 9 |
|
| 10 |
HUB_URL = "http://localhost:8000"
|
| 11 |
|
| 12 |
async def test():
|
| 13 |
+
provider = MEPIdentity(f"test_provider_{uuid.uuid4().hex[:6]}.pem")
|
| 14 |
+
consumer = MEPIdentity(f"test_consumer_{uuid.uuid4().hex[:6]}.pem")
|
| 15 |
+
requests.post(f'{HUB_URL}/register', json={'pubkey': provider.pub_pem})
|
| 16 |
+
requests.post(f'{HUB_URL}/register', json={'pubkey': consumer.pub_pem})
|
| 17 |
+
ts = str(int(time.time()))
|
| 18 |
+
sig = provider.sign(provider.node_id, ts)
|
| 19 |
+
sig_safe = urllib.parse.quote(sig)
|
| 20 |
+
async with websockets.connect(f'ws://localhost:8000/ws/{provider.node_id}?timestamp={ts}&signature={sig_safe}') as ws:
|
| 21 |
+
submit_payload = json.dumps({'consumer_id': consumer.node_id, 'payload': 'Test payload', 'bounty': 1.0})
|
| 22 |
+
submit_headers = consumer.get_auth_headers(submit_payload)
|
| 23 |
+
submit_headers["Content-Type"] = "application/json"
|
| 24 |
+
requests.post(f'{HUB_URL}/tasks/submit', data=submit_payload, headers=submit_headers)
|
| 25 |
|
| 26 |
msg = await asyncio.wait_for(ws.recv(), timeout=2.0)
|
| 27 |
data = json.loads(msg)
|
|
|
|
| 29 |
|
| 30 |
if data['event'] == 'rfc':
|
| 31 |
task_id = data['data']['id']
|
| 32 |
+
bid_payload = json.dumps({'task_id': task_id, 'provider_id': provider.node_id})
|
| 33 |
+
bid_headers = provider.get_auth_headers(bid_payload)
|
| 34 |
+
bid_headers["Content-Type"] = "application/json"
|
| 35 |
+
resp = requests.post(f'{HUB_URL}/tasks/bid', data=bid_payload, headers=bid_headers)
|
| 36 |
print('Bid response:', resp.json())
|
| 37 |
|
| 38 |
+
complete_payload = json.dumps({
|
| 39 |
+
'task_id': task_id,
|
| 40 |
+
'provider_id': provider.node_id,
|
| 41 |
'result_payload': 'Done!'
|
| 42 |
})
|
| 43 |
+
complete_headers = provider.get_auth_headers(complete_payload)
|
| 44 |
+
complete_headers["Content-Type"] = "application/json"
|
| 45 |
+
complete_resp = requests.post(f'{HUB_URL}/tasks/complete', data=complete_payload, headers=complete_headers)
|
| 46 |
print('Complete response:', complete_resp.json())
|
| 47 |
|
| 48 |
if __name__ == '__main__':
|
node/test_crypto.py
CHANGED
|
@@ -26,7 +26,7 @@ async def test():
|
|
| 26 |
|
| 27 |
ws_url = f"ws://localhost:8000/ws/{bot.node_id}?timestamp={ts}&signature={sig_safe}"
|
| 28 |
try:
|
| 29 |
-
async with websockets.connect(ws_url)
|
| 30 |
print("β
WebSocket Authenticated!")
|
| 31 |
|
| 32 |
# 4. Submit Task
|
|
|
|
| 26 |
|
| 27 |
ws_url = f"ws://localhost:8000/ws/{bot.node_id}?timestamp={ts}&signature={sig_safe}"
|
| 28 |
try:
|
| 29 |
+
async with websockets.connect(ws_url):
|
| 30 |
print("β
WebSocket Authenticated!")
|
| 31 |
|
| 32 |
# 4. Submit Task
|
node/test_dm.py
CHANGED
|
@@ -3,6 +3,9 @@ import websockets
|
|
| 3 |
import json
|
| 4 |
import requests
|
| 5 |
import uuid
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
HUB_URL = "http://localhost:8000"
|
| 8 |
WS_URL = "ws://localhost:8000"
|
|
@@ -11,44 +14,57 @@ async def test_direct_message():
|
|
| 11 |
print("=== Testing MEP Direct Messaging (Zero Bounty) ===")
|
| 12 |
|
| 13 |
# 1. Start Alice (Provider)
|
| 14 |
-
|
| 15 |
-
# Registration happens automatically now via Identity module, json={"pubkey": alice_id})
|
| 16 |
|
| 17 |
# 2. Start Bob (Consumer)
|
| 18 |
-
|
| 19 |
-
# Registration happens automatically now via Identity module, json={"pubkey": bob_id})
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
async def alice_listen():
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
print("π§ Alice: Online and listening...")
|
| 26 |
msg = await asyncio.wait_for(ws.recv(), timeout=5)
|
| 27 |
data = json.loads(msg)
|
| 28 |
|
| 29 |
-
print(
|
| 30 |
print(f"π§ Alice: Payload: {data['data']['payload']}")
|
| 31 |
print(f"π§ Alice: Bounty: {data['data']['bounty']} SECONDS")
|
| 32 |
|
| 33 |
# Alice replies for free
|
| 34 |
-
|
| 35 |
"task_id": data['data']['id'],
|
| 36 |
-
"provider_id":
|
| 37 |
"result_payload": "Yes Bob, I am available for a meeting tomorrow at 2 PM. Free of charge! π±"
|
| 38 |
})
|
|
|
|
|
|
|
|
|
|
| 39 |
print("π§ Alice: Sent reply!")
|
| 40 |
|
| 41 |
async def bob_listen():
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
| 43 |
# Bob submits a direct task to Alice with 0 bounty
|
| 44 |
await asyncio.sleep(1) # Let Alice connect first
|
| 45 |
print("π¦ Bob: Sending Direct Message to Alice (0.0 SECONDS)...")
|
| 46 |
-
|
| 47 |
-
"consumer_id":
|
| 48 |
"payload": "Hey Alice, are you free for a meeting tomorrow at 2 PM?",
|
| 49 |
"bounty": 0.0,
|
| 50 |
-
"target_node":
|
| 51 |
})
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
# Bob waits for Alice's reply
|
| 54 |
msg = await asyncio.wait_for(ws.recv(), timeout=5)
|
|
@@ -60,4 +76,4 @@ async def test_direct_message():
|
|
| 60 |
print("=== Direct Messaging Test Complete! ===")
|
| 61 |
|
| 62 |
if __name__ == "__main__":
|
| 63 |
-
asyncio.run(test_direct_message())
|
|
|
|
| 3 |
import json
|
| 4 |
import requests
|
| 5 |
import uuid
|
| 6 |
+
import time
|
| 7 |
+
import urllib.parse
|
| 8 |
+
from identity import MEPIdentity
|
| 9 |
|
| 10 |
HUB_URL = "http://localhost:8000"
|
| 11 |
WS_URL = "ws://localhost:8000"
|
|
|
|
| 14 |
print("=== Testing MEP Direct Messaging (Zero Bounty) ===")
|
| 15 |
|
| 16 |
# 1. Start Alice (Provider)
|
| 17 |
+
alice = MEPIdentity(f"alice_{uuid.uuid4().hex[:6]}.pem")
|
|
|
|
| 18 |
|
| 19 |
# 2. Start Bob (Consumer)
|
| 20 |
+
bob = MEPIdentity(f"bob_{uuid.uuid4().hex[:6]}.pem")
|
|
|
|
| 21 |
|
| 22 |
+
requests.post(f"{HUB_URL}/register", json={"pubkey": alice.pub_pem})
|
| 23 |
+
requests.post(f"{HUB_URL}/register", json={"pubkey": bob.pub_pem})
|
| 24 |
+
|
| 25 |
+
print(f"β
Registered Alice ({alice.node_id}) and Bob ({bob.node_id})")
|
| 26 |
|
| 27 |
async def alice_listen():
|
| 28 |
+
ts = str(int(time.time()))
|
| 29 |
+
sig = alice.sign(alice.node_id, ts)
|
| 30 |
+
sig_safe = urllib.parse.quote(sig)
|
| 31 |
+
async with websockets.connect(f"{WS_URL}/ws/{alice.node_id}?timestamp={ts}&signature={sig_safe}") as ws:
|
| 32 |
print("π§ Alice: Online and listening...")
|
| 33 |
msg = await asyncio.wait_for(ws.recv(), timeout=5)
|
| 34 |
data = json.loads(msg)
|
| 35 |
|
| 36 |
+
print("π§ Alice: Received DIRECT MESSAGE!")
|
| 37 |
print(f"π§ Alice: Payload: {data['data']['payload']}")
|
| 38 |
print(f"π§ Alice: Bounty: {data['data']['bounty']} SECONDS")
|
| 39 |
|
| 40 |
# Alice replies for free
|
| 41 |
+
payload_str = json.dumps({
|
| 42 |
"task_id": data['data']['id'],
|
| 43 |
+
"provider_id": alice.node_id,
|
| 44 |
"result_payload": "Yes Bob, I am available for a meeting tomorrow at 2 PM. Free of charge! π±"
|
| 45 |
})
|
| 46 |
+
headers = alice.get_auth_headers(payload_str)
|
| 47 |
+
headers["Content-Type"] = "application/json"
|
| 48 |
+
requests.post(f"{HUB_URL}/tasks/complete", data=payload_str, headers=headers)
|
| 49 |
print("π§ Alice: Sent reply!")
|
| 50 |
|
| 51 |
async def bob_listen():
|
| 52 |
+
ts = str(int(time.time()))
|
| 53 |
+
sig = bob.sign(bob.node_id, ts)
|
| 54 |
+
sig_safe = urllib.parse.quote(sig)
|
| 55 |
+
async with websockets.connect(f"{WS_URL}/ws/{bob.node_id}?timestamp={ts}&signature={sig_safe}") as ws:
|
| 56 |
# Bob submits a direct task to Alice with 0 bounty
|
| 57 |
await asyncio.sleep(1) # Let Alice connect first
|
| 58 |
print("π¦ Bob: Sending Direct Message to Alice (0.0 SECONDS)...")
|
| 59 |
+
payload_str = json.dumps({
|
| 60 |
+
"consumer_id": bob.node_id,
|
| 61 |
"payload": "Hey Alice, are you free for a meeting tomorrow at 2 PM?",
|
| 62 |
"bounty": 0.0,
|
| 63 |
+
"target_node": alice.node_id
|
| 64 |
})
|
| 65 |
+
headers = bob.get_auth_headers(payload_str)
|
| 66 |
+
headers["Content-Type"] = "application/json"
|
| 67 |
+
requests.post(f"{HUB_URL}/tasks/submit", data=payload_str, headers=headers)
|
| 68 |
|
| 69 |
# Bob waits for Alice's reply
|
| 70 |
msg = await asyncio.wait_for(ws.recv(), timeout=5)
|
|
|
|
| 76 |
print("=== Direct Messaging Test Complete! ===")
|
| 77 |
|
| 78 |
if __name__ == "__main__":
|
| 79 |
+
asyncio.run(test_direct_message())
|
node/test_three_markets.py
CHANGED
|
@@ -5,6 +5,8 @@ import websockets
|
|
| 5 |
import time
|
| 6 |
import urllib.parse
|
| 7 |
from identity import MEPIdentity
|
|
|
|
|
|
|
| 8 |
|
| 9 |
HUB_URL = "http://localhost:8000"
|
| 10 |
WS_URL = "ws://localhost:8000/ws"
|
|
@@ -15,7 +17,7 @@ def get_auth_url(identity: MEPIdentity):
|
|
| 15 |
sig_safe = urllib.parse.quote(sig)
|
| 16 |
return f"{WS_URL}/{identity.node_id}?timestamp={ts}&signature={sig_safe}"
|
| 17 |
|
| 18 |
-
def submit_task(identity: MEPIdentity, payload: str, bounty: float, target: str = None):
|
| 19 |
data = {
|
| 20 |
"consumer_id": identity.node_id,
|
| 21 |
"payload": payload,
|
|
@@ -62,8 +64,8 @@ async def test_three_markets():
|
|
| 62 |
print("Testing the 3 MEP Markets (+, 0, -)")
|
| 63 |
print("=" * 60)
|
| 64 |
|
| 65 |
-
alice = MEPIdentity("
|
| 66 |
-
bob = MEPIdentity("
|
| 67 |
|
| 68 |
requests.post(f"{HUB_URL}/register", json={"pubkey": alice.pub_pem})
|
| 69 |
requests.post(f"{HUB_URL}/register", json={"pubkey": bob.pub_pem})
|
|
@@ -81,9 +83,9 @@ async def test_three_markets():
|
|
| 81 |
print(f"π¦ Bob: Received Compute RFC {task_id[:8]} for +{data['data']['bounty']} SECONDS")
|
| 82 |
bid_res = place_bid(bob, task_id)
|
| 83 |
if bid_res["status"] == "accepted":
|
| 84 |
-
print(
|
| 85 |
complete_task(bob, task_id, "Here is the code you requested.")
|
| 86 |
-
print(
|
| 87 |
|
| 88 |
# 2. Wait for Cyberspace Direct Message (0.0)
|
| 89 |
msg = await ws.recv()
|
|
@@ -93,7 +95,7 @@ async def test_three_markets():
|
|
| 93 |
print(f"π¦ Bob: Received Cyberspace DM {task_id[:8]} from Alice (0.0 SECONDS)")
|
| 94 |
print(f"π¦ Bob: Message = '{data['data']['payload']}'")
|
| 95 |
complete_task(bob, task_id, "Yes Alice, I am free.")
|
| 96 |
-
print(
|
| 97 |
|
| 98 |
# 3. Wait for Data Market RFC (-2.0)
|
| 99 |
msg = await ws.recv()
|
|
@@ -106,14 +108,14 @@ async def test_three_markets():
|
|
| 106 |
# Bob's local configuration allows him to spend up to 5.0 SECONDS
|
| 107 |
max_purchase_price = -5.0
|
| 108 |
if cost >= max_purchase_price:
|
| 109 |
-
print(
|
| 110 |
bid_res = place_bid(bob, task_id)
|
| 111 |
if bid_res["status"] == "accepted":
|
| 112 |
print(f"π¦ Bob: Paid {abs(cost)} SECONDS to download premium data: '{bid_res['payload']}'")
|
| 113 |
complete_task(bob, task_id, "Data received successfully.")
|
| 114 |
-
print(
|
| 115 |
else:
|
| 116 |
-
print(
|
| 117 |
|
| 118 |
await asyncio.sleep(0.5)
|
| 119 |
|
|
@@ -123,19 +125,19 @@ async def test_three_markets():
|
|
| 123 |
|
| 124 |
async with websockets.connect(get_auth_url(alice)) as ws:
|
| 125 |
# Market 1: Compute Market (+5.0)
|
| 126 |
-
print(
|
| 127 |
submit_task(alice, "Write me a python script", 5.0)
|
| 128 |
-
await asyncio.wait_for(ws.recv(), timeout=
|
| 129 |
|
| 130 |
# Market 2: Cyberspace Market (0.0)
|
| 131 |
-
print(
|
| 132 |
submit_task(alice, "Are you free to chat?", 0.0, target=bob.node_id)
|
| 133 |
-
await asyncio.wait_for(ws.recv(), timeout=
|
| 134 |
|
| 135 |
# Market 3: Data Market (-2.0)
|
| 136 |
-
print(
|
| 137 |
submit_task(alice, "SECRET_TRADING_ALGO_V9", -2.0)
|
| 138 |
-
await asyncio.wait_for(ws.recv(), timeout=
|
| 139 |
|
| 140 |
await asyncio.sleep(0.5)
|
| 141 |
|
|
|
|
| 5 |
import time
|
| 6 |
import urllib.parse
|
| 7 |
from identity import MEPIdentity
|
| 8 |
+
import uuid
|
| 9 |
+
from typing import Optional
|
| 10 |
|
| 11 |
HUB_URL = "http://localhost:8000"
|
| 12 |
WS_URL = "ws://localhost:8000/ws"
|
|
|
|
| 17 |
sig_safe = urllib.parse.quote(sig)
|
| 18 |
return f"{WS_URL}/{identity.node_id}?timestamp={ts}&signature={sig_safe}"
|
| 19 |
|
| 20 |
+
def submit_task(identity: MEPIdentity, payload: str, bounty: float, target: Optional[str] = None):
|
| 21 |
data = {
|
| 22 |
"consumer_id": identity.node_id,
|
| 23 |
"payload": payload,
|
|
|
|
| 64 |
print("Testing the 3 MEP Markets (+, 0, -)")
|
| 65 |
print("=" * 60)
|
| 66 |
|
| 67 |
+
alice = MEPIdentity(f"alice_{uuid.uuid4().hex[:6]}.pem")
|
| 68 |
+
bob = MEPIdentity(f"bob_{uuid.uuid4().hex[:6]}.pem")
|
| 69 |
|
| 70 |
requests.post(f"{HUB_URL}/register", json={"pubkey": alice.pub_pem})
|
| 71 |
requests.post(f"{HUB_URL}/register", json={"pubkey": bob.pub_pem})
|
|
|
|
| 83 |
print(f"π¦ Bob: Received Compute RFC {task_id[:8]} for +{data['data']['bounty']} SECONDS")
|
| 84 |
bid_res = place_bid(bob, task_id)
|
| 85 |
if bid_res["status"] == "accepted":
|
| 86 |
+
print("π¦ Bob: Won Compute Bid! Completing task...")
|
| 87 |
complete_task(bob, task_id, "Here is the code you requested.")
|
| 88 |
+
print("π¦ Bob: Compute task done.\n")
|
| 89 |
|
| 90 |
# 2. Wait for Cyberspace Direct Message (0.0)
|
| 91 |
msg = await ws.recv()
|
|
|
|
| 95 |
print(f"π¦ Bob: Received Cyberspace DM {task_id[:8]} from Alice (0.0 SECONDS)")
|
| 96 |
print(f"π¦ Bob: Message = '{data['data']['payload']}'")
|
| 97 |
complete_task(bob, task_id, "Yes Alice, I am free.")
|
| 98 |
+
print("π¦ Bob: Sent free reply.\n")
|
| 99 |
|
| 100 |
# 3. Wait for Data Market RFC (-2.0)
|
| 101 |
msg = await ws.recv()
|
|
|
|
| 108 |
# Bob's local configuration allows him to spend up to 5.0 SECONDS
|
| 109 |
max_purchase_price = -5.0
|
| 110 |
if cost >= max_purchase_price:
|
| 111 |
+
print("π¦ Bob: Budget allows it! Bidding on premium data...")
|
| 112 |
bid_res = place_bid(bob, task_id)
|
| 113 |
if bid_res["status"] == "accepted":
|
| 114 |
print(f"π¦ Bob: Paid {abs(cost)} SECONDS to download premium data: '{bid_res['payload']}'")
|
| 115 |
complete_task(bob, task_id, "Data received successfully.")
|
| 116 |
+
print("π¦ Bob: Premium data acquisition complete.\n")
|
| 117 |
else:
|
| 118 |
+
print("π¦ Bob: Too expensive. Ignored.")
|
| 119 |
|
| 120 |
await asyncio.sleep(0.5)
|
| 121 |
|
|
|
|
| 125 |
|
| 126 |
async with websockets.connect(get_auth_url(alice)) as ws:
|
| 127 |
# Market 1: Compute Market (+5.0)
|
| 128 |
+
print("π© Alice: Submitting Compute Task (+5.0 SECONDS)...")
|
| 129 |
submit_task(alice, "Write me a python script", 5.0)
|
| 130 |
+
await asyncio.wait_for(ws.recv(), timeout=6.0)
|
| 131 |
|
| 132 |
# Market 2: Cyberspace Market (0.0)
|
| 133 |
+
print("π© Alice: Sending Cyberspace DM to Bob (0.0 SECONDS)...")
|
| 134 |
submit_task(alice, "Are you free to chat?", 0.0, target=bob.node_id)
|
| 135 |
+
await asyncio.wait_for(ws.recv(), timeout=6.0)
|
| 136 |
|
| 137 |
# Market 3: Data Market (-2.0)
|
| 138 |
+
print("π© Alice: Broadcasting Premium Dataset (-2.0 SECONDS)...")
|
| 139 |
submit_task(alice, "SECRET_TRADING_ALGO_V9", -2.0)
|
| 140 |
+
await asyncio.wait_for(ws.recv(), timeout=6.0)
|
| 141 |
|
| 142 |
await asyncio.sleep(0.5)
|
| 143 |
|
pyproject.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[tool.ruff]
|
| 2 |
+
line-length = 100
|
| 3 |
+
target-version = "py310"
|
| 4 |
+
extend-exclude = ["**/__pycache__/*", "**/logs/*", "**/hub_data/*"]
|
| 5 |
+
|
| 6 |
+
[tool.ruff.lint]
|
| 7 |
+
select = ["E", "F"]
|
| 8 |
+
ignore = ["E501"]
|
| 9 |
+
|
| 10 |
+
[tool.mypy]
|
| 11 |
+
python_version = "3.10"
|
| 12 |
+
ignore_missing_imports = true
|
| 13 |
+
warn_unused_ignores = true
|
| 14 |
+
warn_redundant_casts = true
|
| 15 |
+
warn_unused_configs = true
|
| 16 |
+
show_error_codes = true
|
| 17 |
+
explicit_package_bases = true
|
| 18 |
+
disable_error_code = ["import-untyped"]
|
| 19 |
+
files = ["hub", "node", "core", "skills", "tests"]
|
skills/sleeping_api.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import json
|
| 3 |
from typing import Dict, Any
|
| 4 |
|
| 5 |
class SleepingAPI:
|
|
|
|
|
|
|
|
|
|
| 1 |
from typing import Dict, Any
|
| 2 |
|
| 3 |
class SleepingAPI:
|