WUAIBING commited on
Commit
a25490a
Β·
1 Parent(s): ed2f842
.gitignore CHANGED
@@ -1,2 +1,12 @@
1
- \n*.db\n*.sqlite3
 
 
 
 
 
 
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 for **RESEARCH AND EDUCATIONAL PURPOSES ONLY**.
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 agree to:
22
 
23
- 1. **Comply with all applicable laws** in your jurisdiction
24
- 2. **Respect third-party Terms of Service** (OpenAI, Google, Anthropic, etc.)
25
- 3. **Use only for lawful purposes**
26
- 4. **Assume all liability** for your use of this software
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 research purposes
47
- - Non-transferable outside the research context
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 before using this software for any purpose beyond personal research.
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
- Miao Exchange Protocol (MEP) License
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 for **RESEARCH AND EDUCATIONAL PURPOSES ONLY**, subject to the
9
- following additional restrictions:
10
-
11
- ## ADDITIONAL RESTRICTIONS
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
- There are three ways to interact with the MEP network. Choose the one that fits your needs:
30
 
31
- ### Option 1: Run a Standalone Provider Node (Easiest)
32
  Turn your computer into a worker node that earns SECONDS while you sleep.
33
 
34
- 1. **Clone the repository:**
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
- 3. **Start Contributing!**
44
- - To contribute LLM compute: `python3 mep_provider.py`
45
- - To contribute CLI execution (Advanced/Risky): `python3 mep_cli_provider.py`
46
-
47
- *(Note: By default, nodes connect to `ws://localhost:8000`. Edit the `HUB_URL` inside the script to point to a public MEP Hub).*
 
48
 
49
  ---
50
 
51
  ### Option 2: Install the Clawdbot Skill (For Bot Owners)
52
- Integrate MEP directly into your Clawdbot so you can submit tasks from Discord/WeChat and let your bot earn SECONDS autonomously.
53
 
54
- 1. **Copy the Skill:**
55
- Move the `skills/mep-exchange` folder into your Clawdbot's skills directory.
56
- 2. **Configure (Optional):**
57
- Edit `skills/mep-exchange/index.js` to set your preferred Hub URL and `max_purchase_price` if you wish to buy premium data.
58
- 3. **Use the Commands:**
59
  ```bash
60
- [mep] status # Check connection and active tasks
61
- [mep] balance # View your SECONDS balance
62
- [mep] idle start # Tell your bot to earn SECONDS while you sleep
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 an L1 Hub (For Network Operators)
74
- Run the core matchmaking engine and ledger that connects consumers and providers.
75
 
76
- 1. **Clone and Setup:**
 
77
  ```bash
78
  git clone https://github.com/WUAIBING/MEP.git
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  cd MEP/hub
80
- pip install fastapi uvicorn websockets pydantic
81
  ```
82
- 2. **Run the Server:**
 
 
 
 
83
  ```bash
84
  uvicorn main:app --host 0.0.0.0 --port 8000
85
  ```
86
- 3. **Deploy:**
87
- For production, deploy this API to a VPS (e.g., DigitalOcean, AWS) behind an Nginx reverse proxy with SSL (wss://).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 with **Additional Restrictions** (see `LICENSE` file). Commercial resale of API access or creation of financial instruments is strictly prohibited.
 
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
- DB_FILE = "ledger.db"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  def init_db():
7
- conn = sqlite3.connect(DB_FILE)
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
- conn.close()
20
 
21
  def register_node(node_id: str, pub_pem: str) -> float:
22
- conn = sqlite3.connect(DB_FILE)
23
  cursor = conn.cursor()
24
- cursor.execute("SELECT balance FROM ledger WHERE node_id = ?", (node_id,))
 
 
 
25
  row = cursor.fetchone()
26
  if not row:
27
- cursor.execute("INSERT INTO ledger (node_id, pub_pem, balance) VALUES (?, ?, ?)", (node_id, pub_pem, 10.0))
 
 
 
 
 
 
 
 
 
28
  conn.commit()
29
- balance = 10.0
 
30
  else:
31
- balance = row[0]
32
- conn.close()
33
- return balance
 
34
 
35
  def get_pub_pem(node_id: str) -> Optional[str]:
36
- conn = sqlite3.connect(DB_FILE)
37
  cursor = conn.cursor()
38
- cursor.execute("SELECT pub_pem FROM ledger WHERE node_id = ?", (node_id,))
 
 
 
39
  row = cursor.fetchone()
40
- conn.close()
41
  return row[0] if row else None
42
 
43
  def get_balance(node_id: str) -> Optional[float]:
44
- conn = sqlite3.connect(DB_FILE)
45
  cursor = conn.cursor()
46
- cursor.execute("SELECT balance FROM ledger WHERE node_id = ?", (node_id,))
 
 
 
47
  row = cursor.fetchone()
48
- conn.close()
49
  return row[0] if row else None
50
 
51
  def set_balance(node_id: str, balance: float):
52
- # This is mainly for testing now
53
- conn = sqlite3.connect(DB_FILE)
54
  cursor = conn.cursor()
55
- cursor.execute("UPDATE ledger SET balance = ? WHERE node_id = ?", (balance, node_id))
 
 
 
56
  conn.commit()
57
- conn.close()
58
 
59
  def add_balance(node_id: str, amount: float):
60
- conn = sqlite3.connect(DB_FILE)
61
  cursor = conn.cursor()
62
- cursor.execute("UPDATE ledger SET balance = balance + ? WHERE node_id = ?", (amount, node_id))
 
 
 
63
  conn.commit()
64
- conn.close()
65
 
66
  def deduct_balance(node_id: str, amount: float) -> bool:
67
- conn = sqlite3.connect(DB_FILE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  cursor = conn.cursor()
69
- cursor.execute("SELECT balance FROM ledger WHERE node_id = ?", (node_id,))
 
 
 
70
  row = cursor.fetchone()
71
- if row is None or row[0] < amount:
72
- conn.close()
73
- return False
74
-
75
- cursor.execute("UPDATE ledger SET balance = balance - ? WHERE node_id = ?", (amount, node_id))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  conn.commit()
77
- conn.close()
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, hub_logger
7
 
8
- from models import NodeRegistration, TaskCreate, TaskResult, NodeBalance, TaskBid
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(task: TaskCreate, authenticated_node: str = Depends(verify_request)):
 
 
 
 
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
- return {"status": "success", "task_id": task_id, "routed_to": task.target_node}
104
- except:
 
 
 
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
- return {"status": "success", "task_id": task_id}
 
 
 
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(result: TaskResult, authenticated_node: str = Depends(verify_request)):
 
 
 
 
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
- raise HTTPException(status_code=404, detail="Task not found or already claimed")
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return {"status": "success", "earned": task["bounty"], "new_balance": db.get_balance(result.provider_id)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- data = await websocket.receive_text()
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, Dict
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 sys
 
7
  from reputation import ReputationManager
 
8
 
9
  class ChronosNode:
10
  """
11
  Simulated Clawdbot Client (Both Consumer & Provider)
12
  """
13
- def __init__(self, node_id: str, hub_url: str = "http://localhost:8000", ws_url: str = "ws://localhost:8000"):
14
- self.node_id = node_id
 
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.node_id, "alias": "test"})
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
- resp = requests.post(f"{self.hub_url}/tasks/complete", json={
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
- uri = f"{self.ws_url}/ws/{self.node_id}"
 
 
 
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
- resp = requests.post(f"{self.hub_url}/tasks/submit", json={
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("usa_node")
102
  usa_node.is_sleeping = False
103
  usa_node.register()
104
 
105
- asia_node = ChronosNode("asia_node")
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, node_id: str):
21
- self.node_id = node_id
 
22
  self.balance = 0.0
23
  self.is_contributing = True
24
  self.capabilities = ["cli-agent", "bash", "python"]
25
 
26
- # Security: In production, run this inside a Docker container!
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 = # Registration happens automatically now via Identity module, json={"pubkey": self.node_id})
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
- uri = f"{WS_URL}/ws/{self.node_id}"
 
 
 
44
  try:
45
  async with websockets.connect(uri) as ws:
46
- print(f"[CLI Provider] Connected to MEP Hub. Awaiting CLI tasks...")
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
- resp = requests.post(f"{HUB_URL}/tasks/bid", json={
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
- requests.post(f"{HUB_URL}/tasks/complete", json={
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
- provider_id = f"cli-agent-{uuid.uuid4().hex[:6]}"
153
- provider = MEPCLIProvider(provider_id)
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, node_id: str):
22
- self.node_id = node_id
 
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 = # Registration happens automatically now via Identity module, json={"pubkey": self.node_id})
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
- uri = f"{WS_URL}/ws/{self.node_id}"
 
 
 
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
- resp = requests.post(f"{HUB_URL}/tasks/bid", json={
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
- resp = requests.post(f"{HUB_URL}/tasks/complete", json={
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
- # Create a miner with unique ID
158
- provider_id = f"mep-provider-{uuid.uuid4().hex[:8]}"
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.node_id = f"{name}-{uuid.uuid4().hex[:6]}"
 
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.node_id})
31
 
32
  # Connect via WebSocket
33
  start_time = time.time()
34
- async with websockets.connect(f"{WS_URL}/ws/{self.node_id}") as ws:
 
 
 
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
- resp = requests.post(f"{HUB_URL}/tasks/complete", json={
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
- consumer_id = "race-test-consumer"
77
- requests.post(f"{HUB_URL}/register", json={"pubkey": consumer_id})
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
- resp = requests.post(f"{HUB_URL}/tasks/submit", json={
95
- "consumer_id": 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/{consumer_id}")
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.node_id = f"{name}-{uuid.uuid4().hex[:6]}"
 
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.node_id})
28
- self.ws = await websockets.connect(f"{WS_URL}/ws/{self.node_id}")
 
 
 
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
- resp = requests.post(f"{HUB_URL}/tasks/complete", json={
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
- consumer_id = "race-consumer-v2"
91
- requests.post(f"{HUB_URL}/register", json={"pubkey": consumer_id})
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
- resp = requests.post(f"{HUB_URL}/tasks/submit", json={
101
- "consumer_id": 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
- results = await asyncio.gather(*[provider.listen_for_task(task_id, bounty) for provider in providers])
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(f"\nπŸ“Š All response times:")
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/{consumer_id}")
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'mep-provider-{uuid.uuid4().hex[:6]}'
11
- requests.post(f'{HUB_URL}/register', json={'pubkey': provider})
12
- async with websockets.connect(f'ws://localhost:8000/ws/{provider}') as ws:
13
- consumer = 'test-consumer'
14
- requests.post(f'{HUB_URL}/register', json={'pubkey': consumer})
15
- requests.post(f'{HUB_URL}/tasks/submit', json={'consumer_id': consumer, 'payload': 'Test payload', 'bounty': 1.0})
 
 
 
 
 
 
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
- resp = requests.post(f'{HUB_URL}/tasks/bid', json={'task_id': task_id, 'provider_id': provider})
 
 
 
24
  print('Bid response:', resp.json())
25
 
26
- complete_resp = requests.post(f'{HUB_URL}/tasks/complete', json={
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) as ws:
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
- alice_id = "alice-specialist-88"
15
- # Registration happens automatically now via Identity module, json={"pubkey": alice_id})
16
 
17
  # 2. Start Bob (Consumer)
18
- bob_id = "bob-general-12"
19
- # Registration happens automatically now via Identity module, json={"pubkey": bob_id})
20
 
21
- print(f"βœ… Registered Alice ({alice_id}) and Bob ({bob_id})")
 
 
 
22
 
23
  async def alice_listen():
24
- async with websockets.connect(f"{WS_URL}/ws/{alice_id}") as ws:
 
 
 
25
  print("πŸ‘§ Alice: Online and listening...")
26
  msg = await asyncio.wait_for(ws.recv(), timeout=5)
27
  data = json.loads(msg)
28
 
29
- print(f"πŸ‘§ Alice: Received DIRECT MESSAGE!")
30
  print(f"πŸ‘§ Alice: Payload: {data['data']['payload']}")
31
  print(f"πŸ‘§ Alice: Bounty: {data['data']['bounty']} SECONDS")
32
 
33
  # Alice replies for free
34
- requests.post(f"{HUB_URL}/tasks/complete", json={
35
  "task_id": data['data']['id'],
36
- "provider_id": alice_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
- async with websockets.connect(f"{WS_URL}/ws/{bob_id}") as ws:
 
 
 
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
- requests.post(f"{HUB_URL}/tasks/submit", json={
47
- "consumer_id": bob_id,
48
  "payload": "Hey Alice, are you free for a meeting tomorrow at 2 PM?",
49
  "bounty": 0.0,
50
- "target_node": alice_id
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("alice.pem")
66
- bob = MEPIdentity("bob.pem")
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(f"πŸ‘¦ Bob: Won Compute Bid! Completing task...")
85
  complete_task(bob, task_id, "Here is the code you requested.")
86
- print(f"πŸ‘¦ Bob: Compute task done.\n")
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(f"πŸ‘¦ Bob: Sent free reply.\n")
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(f"πŸ‘¦ Bob: Budget allows it! Bidding on premium data...")
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(f"πŸ‘¦ Bob: Premium data acquisition complete.\n")
115
  else:
116
- print(f"πŸ‘¦ Bob: Too expensive. Ignored.")
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(f"πŸ‘© Alice: Submitting Compute Task (+5.0 SECONDS)...")
127
  submit_task(alice, "Write me a python script", 5.0)
128
- await asyncio.wait_for(ws.recv(), timeout=2.0) # wait for result
129
 
130
  # Market 2: Cyberspace Market (0.0)
131
- print(f"πŸ‘© Alice: Sending Cyberspace DM to Bob (0.0 SECONDS)...")
132
  submit_task(alice, "Are you free to chat?", 0.0, target=bob.node_id)
133
- await asyncio.wait_for(ws.recv(), timeout=2.0) # wait for result
134
 
135
  # Market 3: Data Market (-2.0)
136
- print(f"πŸ‘© Alice: Broadcasting Premium Dataset (-2.0 SECONDS)...")
137
  submit_task(alice, "SECRET_TRADING_ALGO_V9", -2.0)
138
- await asyncio.wait_for(ws.recv(), timeout=2.0) # wait for result
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: