Clawdbot commited on
Commit Β·
4e321e0
1
Parent(s): 4301a95
Add Three Markets integration test to verify economy logic
Browse files- hub/__pycache__/logger.cpython-310.pyc +0 -0
- hub/__pycache__/main.cpython-310.pyc +0 -0
- hub/ledger.db +0 -0
- hub/logs/hub.json +14 -0
- hub/logs/ledger_audit.log +11 -0
- hub/main.py +26 -25
- node/alice.pem +3 -0
- node/bob.pem +3 -0
- node/test_three_markets.py +151 -0
hub/__pycache__/logger.cpython-310.pyc
ADDED
|
Binary file (2.5 kB). View file
|
|
|
hub/__pycache__/main.cpython-310.pyc
CHANGED
|
Binary files a/hub/__pycache__/main.cpython-310.pyc and b/hub/__pycache__/main.cpython-310.pyc differ
|
|
|
hub/ledger.db
CHANGED
|
Binary files a/hub/ledger.db and b/hub/ledger.db differ
|
|
|
hub/logs/hub.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
@@ -23,14 +23,14 @@ async def verify_request(
|
|
| 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)
|
| 28 |
if not pub_pem:
|
| 29 |
raise HTTPException(status_code=401, detail="Unknown Node ID. Please register first.")
|
| 30 |
-
|
| 31 |
if not auth.verify_signature(pub_pem, payload_str, x_mep_timestamp, x_mep_signature):
|
| 32 |
raise HTTPException(status_code=401, detail="Invalid cryptographic signature.")
|
| 33 |
-
|
| 34 |
return x_mep_nodeid
|
| 35 |
|
| 36 |
@app.post("/register")
|
|
@@ -38,10 +38,10 @@ 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)
|
| 41 |
-
|
| 42 |
log_event("node_registered", f"Node {node_id} registered with starting balance {balance}", node_id=node_id, starting_balance=balance)
|
| 43 |
log_audit("REGISTER", node_id, balance, balance, "START_BONUS")
|
| 44 |
-
|
| 45 |
return {"status": "success", "node_id": node_id, "balance": balance}
|
| 46 |
|
| 47 |
@app.get("/balance/{node_id}")
|
|
@@ -58,15 +58,17 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
|
|
| 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")
|
| 65 |
-
|
| 66 |
# If bounty is positive, consumer is PAYING. Check consumer balance.
|
| 67 |
if task.bounty > 0 and consumer_balance < task.bounty:
|
| 68 |
raise HTTPException(status_code=400, detail="Insufficient SECONDS balance to pay for task")
|
| 69 |
-
|
|
|
|
|
|
|
| 70 |
# Note: If bounty is negative, consumer is SELLING data. We don't deduct here.
|
| 71 |
# We will deduct from the provider when they complete the task.
|
| 72 |
if task.bounty > 0:
|
|
@@ -79,8 +81,7 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
|
|
| 79 |
log_audit("ESCROW", task.consumer_id, -task.bounty, new_balance, task_id)
|
| 80 |
|
| 81 |
log_event("task_submitted", f"Task {task_id[:8]} broadcasted by {task.consumer_id} for {task.bounty}", consumer_id=task.consumer_id, task_id=task_id, bounty=task.bounty)
|
| 82 |
-
|
| 83 |
-
task_id = str(uuid.uuid4())
|
| 84 |
task_data = {
|
| 85 |
"id": task_id,
|
| 86 |
"consumer_id": task.consumer_id,
|
|
@@ -91,7 +92,7 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
|
|
| 91 |
"model_requirement": task.model_requirement
|
| 92 |
}
|
| 93 |
active_tasks[task_id] = task_data
|
| 94 |
-
|
| 95 |
# Target specific node if requested (Direct Message skips bidding)
|
| 96 |
if task.target_node:
|
| 97 |
if task.target_node in connected_nodes:
|
|
@@ -118,27 +119,27 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
|
|
| 118 |
await ws.send_json({"event": "rfc", "data": rfc_data})
|
| 119 |
except:
|
| 120 |
pass
|
| 121 |
-
|
| 122 |
return {"status": "success", "task_id": task_id}
|
| 123 |
|
| 124 |
@app.post("/tasks/bid")
|
| 125 |
async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_request)):
|
| 126 |
if authenticated_node != bid.provider_id:
|
| 127 |
raise HTTPException(status_code=403, detail="Cannot bid on behalf of another node")
|
| 128 |
-
|
| 129 |
task = active_tasks.get(bid.task_id)
|
| 130 |
if not task:
|
| 131 |
raise HTTPException(status_code=404, detail="Task not found or already completed")
|
| 132 |
-
|
| 133 |
if task["status"] != "bidding":
|
| 134 |
return {"status": "rejected", "detail": "Task already assigned to another node"}
|
| 135 |
-
|
| 136 |
# Phase 2 Fast Auction: Accept the first valid bid
|
| 137 |
task["status"] = "assigned"
|
| 138 |
task["provider_id"] = bid.provider_id
|
| 139 |
-
|
| 140 |
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"])
|
| 141 |
-
|
| 142 |
# Return the full payload to the winner
|
| 143 |
return {
|
| 144 |
"status": "accepted",
|
|
@@ -151,11 +152,11 @@ async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_reque
|
|
| 151 |
async def complete_task(result: TaskResult, authenticated_node: str = Depends(verify_request)):
|
| 152 |
if authenticated_node != result.provider_id:
|
| 153 |
raise HTTPException(status_code=403, detail="Cannot complete tasks on behalf of another node")
|
| 154 |
-
|
| 155 |
task = active_tasks.get(result.task_id)
|
| 156 |
if not task:
|
| 157 |
raise HTTPException(status_code=404, detail="Task not found or already claimed")
|
| 158 |
-
|
| 159 |
provider_balance = db.get_balance(result.provider_id)
|
| 160 |
if provider_balance is None:
|
| 161 |
db.set_balance(result.provider_id, 0.0)
|
|
@@ -174,14 +175,14 @@ async def complete_task(result: TaskResult, authenticated_node: str = Depends(ve
|
|
| 174 |
if not success:
|
| 175 |
log_event("data_purchase_failed", f"Provider {result.provider_id} lacks SECONDS to buy {result.task_id}", task_id=result.task_id, provider_id=result.provider_id, cost=cost)
|
| 176 |
raise HTTPException(status_code=400, detail="Provider lacks SECONDS to buy this data")
|
| 177 |
-
|
| 178 |
p_balance = db.get_balance(result.provider_id)
|
| 179 |
log_audit("BUY_DATA", result.provider_id, -cost, p_balance, result.task_id)
|
| 180 |
-
|
| 181 |
db.add_balance(task["consumer_id"], cost) # The sender earns SECONDS
|
| 182 |
c_balance = db.get_balance(task["consumer_id"])
|
| 183 |
log_audit("SELL_DATA", task["consumer_id"], cost, c_balance, result.task_id)
|
| 184 |
-
|
| 185 |
log_event("task_completed", f"Task {result.task_id[:8]} completed by {result.provider_id}", task_id=result.task_id, provider_id=result.provider_id, bounty=bounty)
|
| 186 |
|
| 187 |
# Move task to completed
|
|
@@ -190,7 +191,7 @@ async def complete_task(result: TaskResult, authenticated_node: str = Depends(ve
|
|
| 190 |
task["result"] = result.result_payload
|
| 191 |
completed_tasks[result.task_id] = task
|
| 192 |
del active_tasks[result.task_id]
|
| 193 |
-
|
| 194 |
# ROUTE RESULT BACK TO CONSUMER VIA WEBSOCKET
|
| 195 |
consumer_id = task["consumer_id"]
|
| 196 |
if consumer_id in connected_nodes:
|
|
@@ -215,11 +216,11 @@ async def websocket_endpoint(websocket: WebSocket, node_id: str, timestamp: str,
|
|
| 215 |
if not pub_pem:
|
| 216 |
await websocket.close(code=4001, reason="Unknown Node ID")
|
| 217 |
return
|
| 218 |
-
|
| 219 |
if not auth.verify_signature(pub_pem, node_id, timestamp, signature):
|
| 220 |
await websocket.close(code=4002, reason="Invalid Signature")
|
| 221 |
return
|
| 222 |
-
|
| 223 |
await websocket.accept()
|
| 224 |
connected_nodes[node_id] = websocket
|
| 225 |
try:
|
|
|
|
| 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)
|
| 28 |
if not pub_pem:
|
| 29 |
raise HTTPException(status_code=401, detail="Unknown Node ID. Please register first.")
|
| 30 |
+
|
| 31 |
if not auth.verify_signature(pub_pem, payload_str, x_mep_timestamp, x_mep_signature):
|
| 32 |
raise HTTPException(status_code=401, detail="Invalid cryptographic signature.")
|
| 33 |
+
|
| 34 |
return x_mep_nodeid
|
| 35 |
|
| 36 |
@app.post("/register")
|
|
|
|
| 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)
|
| 41 |
+
|
| 42 |
log_event("node_registered", f"Node {node_id} registered with starting balance {balance}", node_id=node_id, starting_balance=balance)
|
| 43 |
log_audit("REGISTER", node_id, balance, balance, "START_BONUS")
|
| 44 |
+
|
| 45 |
return {"status": "success", "node_id": node_id, "balance": balance}
|
| 46 |
|
| 47 |
@app.get("/balance/{node_id}")
|
|
|
|
| 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")
|
| 65 |
+
|
| 66 |
# If bounty is positive, consumer is PAYING. Check consumer balance.
|
| 67 |
if task.bounty > 0 and consumer_balance < task.bounty:
|
| 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.
|
| 74 |
if task.bounty > 0:
|
|
|
|
| 81 |
log_audit("ESCROW", task.consumer_id, -task.bounty, new_balance, task_id)
|
| 82 |
|
| 83 |
log_event("task_submitted", f"Task {task_id[:8]} broadcasted by {task.consumer_id} for {task.bounty}", consumer_id=task.consumer_id, task_id=task_id, bounty=task.bounty)
|
| 84 |
+
|
|
|
|
| 85 |
task_data = {
|
| 86 |
"id": task_id,
|
| 87 |
"consumer_id": task.consumer_id,
|
|
|
|
| 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)
|
| 97 |
if task.target_node:
|
| 98 |
if task.target_node in connected_nodes:
|
|
|
|
| 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)):
|
| 127 |
if authenticated_node != bid.provider_id:
|
| 128 |
raise HTTPException(status_code=403, detail="Cannot bid on behalf of another node")
|
| 129 |
+
|
| 130 |
task = active_tasks.get(bid.task_id)
|
| 131 |
if not task:
|
| 132 |
raise HTTPException(status_code=404, detail="Task not found or already completed")
|
| 133 |
+
|
| 134 |
if task["status"] != "bidding":
|
| 135 |
return {"status": "rejected", "detail": "Task already assigned to another node"}
|
| 136 |
+
|
| 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 |
+
|
| 143 |
# Return the full payload to the winner
|
| 144 |
return {
|
| 145 |
"status": "accepted",
|
|
|
|
| 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:
|
| 162 |
db.set_balance(result.provider_id, 0.0)
|
|
|
|
| 175 |
if not success:
|
| 176 |
log_event("data_purchase_failed", f"Provider {result.provider_id} lacks SECONDS to buy {result.task_id}", task_id=result.task_id, provider_id=result.provider_id, cost=cost)
|
| 177 |
raise HTTPException(status_code=400, detail="Provider lacks SECONDS to buy this data")
|
| 178 |
+
|
| 179 |
p_balance = db.get_balance(result.provider_id)
|
| 180 |
log_audit("BUY_DATA", result.provider_id, -cost, p_balance, result.task_id)
|
| 181 |
+
|
| 182 |
db.add_balance(task["consumer_id"], cost) # The sender earns SECONDS
|
| 183 |
c_balance = db.get_balance(task["consumer_id"])
|
| 184 |
log_audit("SELL_DATA", task["consumer_id"], cost, c_balance, result.task_id)
|
| 185 |
+
|
| 186 |
log_event("task_completed", f"Task {result.task_id[:8]} completed by {result.provider_id}", task_id=result.task_id, provider_id=result.provider_id, bounty=bounty)
|
| 187 |
|
| 188 |
# Move task to completed
|
|
|
|
| 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"]
|
| 197 |
if consumer_id in connected_nodes:
|
|
|
|
| 216 |
if not pub_pem:
|
| 217 |
await websocket.close(code=4001, reason="Unknown Node ID")
|
| 218 |
return
|
| 219 |
+
|
| 220 |
if not auth.verify_signature(pub_pem, node_id, timestamp, signature):
|
| 221 |
await websocket.close(code=4002, reason="Invalid Signature")
|
| 222 |
return
|
| 223 |
+
|
| 224 |
await websocket.accept()
|
| 225 |
connected_nodes[node_id] = websocket
|
| 226 |
try:
|
node/alice.pem
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN PRIVATE KEY-----
|
| 2 |
+
MC4CAQAwBQYDK2VwBCIEINL0Z70Fzpal0mk+yjhLvB3wbVVlghhYcMd9n7f6UjAM
|
| 3 |
+
-----END PRIVATE KEY-----
|
node/bob.pem
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN PRIVATE KEY-----
|
| 2 |
+
MC4CAQAwBQYDK2VwBCIEIB4LQ5b/vBp7/S89Z/RAGI4/eg6i8MEc40lpzxlWHpTC
|
| 3 |
+
-----END PRIVATE KEY-----
|
node/test_three_markets.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
import requests
|
| 4 |
+
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"
|
| 11 |
+
|
| 12 |
+
def get_auth_url(identity: MEPIdentity):
|
| 13 |
+
ts = str(int(time.time()))
|
| 14 |
+
sig = identity.sign(identity.node_id, ts)
|
| 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,
|
| 22 |
+
"bounty": bounty
|
| 23 |
+
}
|
| 24 |
+
if target:
|
| 25 |
+
data["target_node"] = target
|
| 26 |
+
|
| 27 |
+
payload_str = json.dumps(data)
|
| 28 |
+
headers = identity.get_auth_headers(payload_str)
|
| 29 |
+
headers["Content-Type"] = "application/json"
|
| 30 |
+
r = requests.post(f"{HUB_URL}/tasks/submit", data=payload_str, headers=headers)
|
| 31 |
+
return r.json()
|
| 32 |
+
|
| 33 |
+
def place_bid(identity: MEPIdentity, task_id: str):
|
| 34 |
+
data = {
|
| 35 |
+
"task_id": task_id,
|
| 36 |
+
"provider_id": identity.node_id
|
| 37 |
+
}
|
| 38 |
+
payload_str = json.dumps(data)
|
| 39 |
+
headers = identity.get_auth_headers(payload_str)
|
| 40 |
+
headers["Content-Type"] = "application/json"
|
| 41 |
+
r = requests.post(f"{HUB_URL}/tasks/bid", data=payload_str, headers=headers)
|
| 42 |
+
return r.json()
|
| 43 |
+
|
| 44 |
+
def complete_task(identity: MEPIdentity, task_id: str, result: str):
|
| 45 |
+
data = {
|
| 46 |
+
"task_id": task_id,
|
| 47 |
+
"provider_id": identity.node_id,
|
| 48 |
+
"result_payload": result
|
| 49 |
+
}
|
| 50 |
+
payload_str = json.dumps(data)
|
| 51 |
+
headers = identity.get_auth_headers(payload_str)
|
| 52 |
+
headers["Content-Type"] = "application/json"
|
| 53 |
+
r = requests.post(f"{HUB_URL}/tasks/complete", data=payload_str, headers=headers)
|
| 54 |
+
return r.json()
|
| 55 |
+
|
| 56 |
+
def get_balance(identity: MEPIdentity):
|
| 57 |
+
r = requests.get(f"{HUB_URL}/balance/{identity.node_id}")
|
| 58 |
+
return r.json().get("balance_seconds", 0.0)
|
| 59 |
+
|
| 60 |
+
async def test_three_markets():
|
| 61 |
+
print("=" * 60)
|
| 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})
|
| 70 |
+
|
| 71 |
+
print(f"π© Alice (Consumer): {alice.node_id} | Starting Bal: {get_balance(alice)}")
|
| 72 |
+
print(f"π¦ Bob (Provider): {bob.node_id} | Starting Bal: {get_balance(bob)}\n")
|
| 73 |
+
|
| 74 |
+
async def bob_listener():
|
| 75 |
+
async with websockets.connect(get_auth_url(bob)) as ws:
|
| 76 |
+
# 1. Wait for Compute Market RFC (+5.0)
|
| 77 |
+
msg = await ws.recv()
|
| 78 |
+
data = json.loads(msg)
|
| 79 |
+
if data["event"] == "rfc" and data["data"]["bounty"] > 0:
|
| 80 |
+
task_id = data["data"]["id"]
|
| 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()
|
| 90 |
+
data = json.loads(msg)
|
| 91 |
+
if data["event"] == "new_task" and data["data"]["bounty"] == 0.0:
|
| 92 |
+
task_id = data["data"]["id"]
|
| 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()
|
| 100 |
+
data = json.loads(msg)
|
| 101 |
+
if data["event"] == "rfc" and data["data"]["bounty"] < 0:
|
| 102 |
+
task_id = data["data"]["id"]
|
| 103 |
+
cost = data["data"]["bounty"]
|
| 104 |
+
print(f"π¦ Bob: Received Data Market RFC {task_id[:8]} costing {cost} SECONDS")
|
| 105 |
+
|
| 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 |
+
|
| 120 |
+
async def alice_sender():
|
| 121 |
+
# Let Bob connect
|
| 122 |
+
await asyncio.sleep(0.5)
|
| 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 |
+
|
| 142 |
+
await asyncio.gather(bob_listener(), alice_sender())
|
| 143 |
+
|
| 144 |
+
print("=" * 60)
|
| 145 |
+
print("Final Balances:")
|
| 146 |
+
print(f"π© Alice (Started 10.0): {get_balance(alice)} (Paid 5.0, Earned 2.0 = Expected 7.0)")
|
| 147 |
+
print(f"π¦ Bob (Started 10.0): {get_balance(bob)} (Earned 5.0, Paid 2.0 = Expected 13.0)")
|
| 148 |
+
print("=" * 60)
|
| 149 |
+
|
| 150 |
+
if __name__ == "__main__":
|
| 151 |
+
asyncio.run(test_three_markets())
|