按显卡世代和创世编号结算回本,机房批量接入需审核网段并用硬件指纹防克隆。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+256
-11
@@ -25,9 +25,11 @@ from app.clock import utcnow
|
||||
from app.database import Base, SessionLocal, engine
|
||||
from app.main import app
|
||||
from app.jobs.daily_yield import run_daily_yield_cron
|
||||
from app.money import money
|
||||
from app.models import (
|
||||
ComputeNode,
|
||||
ComputeTask,
|
||||
IPWhitelist,
|
||||
NodeStatus,
|
||||
NodeYield,
|
||||
PushStatus,
|
||||
@@ -36,6 +38,8 @@ from app.models import (
|
||||
YieldStatus,
|
||||
)
|
||||
from app.services.heartbeat import mark_stale_nodes
|
||||
from app.services.datacenter import FINGERPRINT_CLASH
|
||||
from app.services.ip_guard import DATACENTER_BATCH_MESSAGE
|
||||
from app.services.tasks import dispatch_random_tasks
|
||||
|
||||
|
||||
@@ -45,6 +49,9 @@ class CoreFlowTest(unittest.TestCase):
|
||||
Base.metadata.create_all(bind=engine)
|
||||
self.client_cm = TestClient(app)
|
||||
self.client = self.client_cm.__enter__()
|
||||
with SessionLocal() as session:
|
||||
session.add(IPWhitelist(ip_address="testclient", note="unit test", created_at=utcnow()))
|
||||
session.commit()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.client_cm.__exit__(None, None, None)
|
||||
@@ -147,18 +154,18 @@ class CoreFlowTest(unittest.TestCase):
|
||||
self.assertEqual(full_row.online_rate, Decimal("1.00000000"))
|
||||
self.assertEqual(full_row.task_response_rate, Decimal("1.00000000"))
|
||||
self.assertEqual(full_row.performance, Decimal("1.00000000"))
|
||||
self.assertEqual(full_row.points_delta, Decimal("1.00000000"))
|
||||
self.assertEqual(full_row.points_delta, money(Decimal(365) / Decimal(30)))
|
||||
self.assertEqual(full_row.compute_output, Decimal("365.00000000"))
|
||||
self.assertEqual(full_row.online_weight, Decimal("0.70000000"))
|
||||
|
||||
self.assertEqual(idle_row.tasks_dispatched, 0)
|
||||
self.assertEqual(idle_row.performance, Decimal("1.00000000"))
|
||||
self.assertEqual(idle_row.points_delta, Decimal("1.00000000"))
|
||||
self.assertEqual(idle_row.points_delta, money(Decimal(365) / Decimal(30)))
|
||||
self.assertEqual(idle_row.online_weight, Decimal("1.00000000"))
|
||||
|
||||
self.assertEqual(miss_row.task_response_rate, Decimal("0.00000000"))
|
||||
self.assertEqual(miss_row.performance, Decimal("0.70000000"))
|
||||
self.assertEqual(miss_row.points_delta, Decimal("0.70000000"))
|
||||
self.assertEqual(miss_row.points_delta, money(Decimal(365) / Decimal(30) * Decimal("0.7")))
|
||||
self.assertEqual(miss_row.compute_output, Decimal("255.50000000"))
|
||||
|
||||
again = self.client.post(
|
||||
@@ -198,7 +205,7 @@ class CoreFlowTest(unittest.TestCase):
|
||||
row = self._yield_row("NODE-HALF")
|
||||
self.assertEqual(row.online_seconds, 12 * 60 * 60)
|
||||
self.assertEqual(row.online_rate, Decimal("0.50000000"))
|
||||
self.assertEqual(row.points_delta, Decimal("0.50000000"))
|
||||
self.assertEqual(row.points_delta, money(Decimal(365) / Decimal(30) * Decimal("0.5")))
|
||||
|
||||
def test_daily_cron_writes_previous_closed_day(self) -> None:
|
||||
# 北京时间 2026-10-12 00:05,对应上一个已结束的 UTC 账本日 2026-10-10。
|
||||
@@ -213,7 +220,7 @@ class CoreFlowTest(unittest.TestCase):
|
||||
self.assertFalse(summary.is_partial)
|
||||
row = self._yield_row("NODE-CRON")
|
||||
self.assertEqual(row.status, YieldStatus.SETTLED)
|
||||
self.assertEqual(row.points_delta, Decimal("1.00000000"))
|
||||
self.assertEqual(row.points_delta, money(Decimal(365) / Decimal(30)))
|
||||
self.assertEqual(row.compute_output, Decimal("365.00000000"))
|
||||
|
||||
again = run_daily_yield_cron(now=moment)
|
||||
@@ -236,7 +243,7 @@ class CoreFlowTest(unittest.TestCase):
|
||||
too_much = self.client.post(
|
||||
"/api/v1/withdrawals",
|
||||
headers=payer,
|
||||
json={"wallet_address": "0xpay", "amount": "10"},
|
||||
json={"wallet_address": "0xpay", "amount": "100"},
|
||||
)
|
||||
self.assertEqual(too_much.status_code, 400, too_much.text)
|
||||
|
||||
@@ -247,12 +254,13 @@ class CoreFlowTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(created.status_code, 200, created.text)
|
||||
withdrawal_id = created.json()["id"]
|
||||
self.assertEqual(created.json()["available_after"], "0.60000000")
|
||||
released = money(Decimal(365) / Decimal(30))
|
||||
self.assertEqual(created.json()["available_after"], format(money(released - Decimal("0.4")), "f"))
|
||||
|
||||
blocked = self.client.post(
|
||||
"/api/v1/withdrawals",
|
||||
headers=payer,
|
||||
json={"wallet_address": "0xpay", "amount": "0.7"},
|
||||
json={"wallet_address": "0xpay", "amount": "12"},
|
||||
)
|
||||
self.assertEqual(blocked.status_code, 400, blocked.text)
|
||||
|
||||
@@ -266,7 +274,7 @@ class CoreFlowTest(unittest.TestCase):
|
||||
self.assertEqual(confirmed.json()["tx_hash"], "0xabc123")
|
||||
|
||||
balance = self.client.get("/api/v1/users/0xpay/balance", headers=self._auth("0xpay"))
|
||||
self.assertEqual(balance.json()["available_points"], "0.60000000")
|
||||
self.assertEqual(balance.json()["available_points"], format(money(released - Decimal("0.4")), "f"))
|
||||
|
||||
def test_roles_split_user_and_admin(self) -> None:
|
||||
registered = self.client.post(
|
||||
@@ -325,6 +333,10 @@ class CoreFlowTest(unittest.TestCase):
|
||||
board = self.client.get("/api/v1/me/dashboard", headers=headers)
|
||||
node = board.json()["nodes"][0]
|
||||
self.assertEqual(node["sn"], sn)
|
||||
self.assertEqual(node["display_node_id"], 1001)
|
||||
self.assertTrue(node["genesis"])
|
||||
self.assertEqual(node["recovery_days"], 30)
|
||||
self.assertEqual(board.json()["network_nodes"], 1001)
|
||||
self.assertTrue(node["activated"])
|
||||
self.assertTrue(node["node_token"])
|
||||
self.assertEqual(node["license_months"], 12)
|
||||
@@ -338,6 +350,8 @@ class CoreFlowTest(unittest.TestCase):
|
||||
client = self.client.get(f"/api/v1/me/nodes/{sn}/client", headers=headers)
|
||||
self.assertEqual(client.status_code, 200, client.text)
|
||||
self.assertIn(node["node_token"], client.text)
|
||||
self.assertIn("nvidia-smi", client.text)
|
||||
self.assertIn("pynvml", client.text)
|
||||
|
||||
duplicated = self.client.post("/api/v1/me/orders", headers=headers, json={"tx_id": "paytx1500usdt01"})
|
||||
self.assertEqual(duplicated.status_code, 409, duplicated.text)
|
||||
@@ -352,6 +366,237 @@ class CoreFlowTest(unittest.TestCase):
|
||||
self.assertEqual(rejected.json()["status"], "rejected")
|
||||
self.assertIsNone(rejected.json()["sn"])
|
||||
|
||||
def test_gpu_generation_locks_recovery_and_releases_stake(self) -> None:
|
||||
from app.services.hardware import classify_generation, score_hardware
|
||||
|
||||
self.assertEqual(classify_generation("NVIDIA GeForce RTX 4090"), ("40", 210))
|
||||
self.assertEqual(classify_generation("NVIDIA GeForce RTX 4070"), ("40", 210))
|
||||
self.assertEqual(classify_generation("NVIDIA GeForce RTX 3060 Ti"), ("30", 365))
|
||||
self.assertEqual(classify_generation("NVIDIA GeForce RTX 3060"), ("30", 365))
|
||||
self.assertEqual(classify_generation("NVIDIA GeForce GTX 1080"), ("10", 540))
|
||||
self.assertEqual(classify_generation("NVIDIA GeForce GTX 1050"), ("10", 540))
|
||||
self.assertEqual(classify_generation("Intel UHD Graphics 630"), ("10", 540))
|
||||
self.assertEqual(score_hardware("NVIDIA GeForce RTX 3060 Ti", 8), Decimal("1"))
|
||||
|
||||
yesterday = (utcnow() - timedelta(days=1)).date()
|
||||
day_start = utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=1)
|
||||
day_end = day_start + timedelta(days=1)
|
||||
self._register("0xgen", "NODE-GEN", "1500")
|
||||
for index in range(19):
|
||||
self._register(f"0xpad{index:02d}", f"PAD-{index:02d}", "1500")
|
||||
self._register("0x40", "NODE-40", "1500")
|
||||
self._register("0x10", "NODE-10", "1500")
|
||||
self._cover_full_day("NODE-GEN", day_start, day_end)
|
||||
self._cover_full_day("NODE-40", day_start, day_end)
|
||||
self._cover_full_day("NODE-10", day_start, day_end)
|
||||
|
||||
genesis = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
json={"sn": "NODE-GEN", "gpu_model": "NVIDIA GeForce RTX 4090", "vram_gb": 24},
|
||||
)
|
||||
self.assertEqual(genesis.status_code, 200, genesis.text)
|
||||
self.assertEqual(genesis.json()["display_node_id"], 1001)
|
||||
self.assertEqual(genesis.json()["gpu_generation"], "40")
|
||||
self.assertEqual(genesis.json()["recovery_days"], 30)
|
||||
still = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
json={"sn": "NODE-GEN", "gpu_model": "NVIDIA GeForce GTX 1050", "vram_gb": 2},
|
||||
)
|
||||
self.assertEqual(still.json()["recovery_days"], 30)
|
||||
|
||||
high = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
json={"sn": "NODE-40", "gpu_model": "NVIDIA GeForce RTX 4090", "vram_gb": 24, "cpu_count": 16, "memory_gb": 64},
|
||||
)
|
||||
self.assertEqual(high.status_code, 200, high.text)
|
||||
self.assertEqual(high.json()["display_node_id"], 1021)
|
||||
self.assertEqual(high.json()["gpu_generation"], "40")
|
||||
self.assertEqual(high.json()["recovery_days"], 210)
|
||||
self.assertEqual(high.json()["recoverable_stake_amount"], "1500.00000000")
|
||||
locked = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
json={"sn": "NODE-40", "gpu_model": "NVIDIA GeForce GTX 1050", "vram_gb": 2},
|
||||
)
|
||||
self.assertEqual(locked.json()["gpu_generation"], "40")
|
||||
self.assertEqual(locked.json()["recovery_days"], 210)
|
||||
|
||||
low = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
json={"sn": "NODE-10", "gpu_model": "NVIDIA GeForce GTX 1050", "vram_gb": 2},
|
||||
)
|
||||
self.assertEqual(low.status_code, 200, low.text)
|
||||
self.assertEqual(low.json()["display_node_id"], 1022)
|
||||
self.assertEqual(low.json()["gpu_generation"], "10")
|
||||
self.assertEqual(low.json()["recovery_days"], 540)
|
||||
|
||||
settled = self.client.post(
|
||||
"/api/v1/yield/settle",
|
||||
headers=self._admin_headers(),
|
||||
params={"yield_date": yesterday.isoformat()},
|
||||
)
|
||||
self.assertEqual(settled.status_code, 200, settled.text)
|
||||
genesis_points = money(Decimal(1500) / Decimal(30))
|
||||
high_points = money(Decimal(1500) / Decimal(210))
|
||||
low_points = money(Decimal(1500) / Decimal(540))
|
||||
self.assertEqual(genesis_points, Decimal("50.00000000"))
|
||||
self.assertEqual(self._yield_row("NODE-GEN").points_delta, genesis_points)
|
||||
self.assertEqual(self._yield_row("NODE-40").points_delta, high_points)
|
||||
self.assertEqual(self._yield_row("NODE-10").points_delta, low_points)
|
||||
with SessionLocal() as session:
|
||||
first = session.scalar(select(ComputeNode).where(ComputeNode.sn == "NODE-GEN"))
|
||||
fast = session.scalar(select(ComputeNode).where(ComputeNode.sn == "NODE-40"))
|
||||
slow = session.scalar(select(ComputeNode).where(ComputeNode.sn == "NODE-10"))
|
||||
self.assertEqual(first.recoverable_stake_amount, money(Decimal(1500) - genesis_points))
|
||||
self.assertEqual(fast.recoverable_stake_amount, money(Decimal(1500) - high_points))
|
||||
self.assertEqual(slow.recoverable_stake_amount, money(Decimal(1500) - low_points))
|
||||
self.assertGreater(slow.license_expires_at, fast.license_expires_at)
|
||||
population = self.client.get("/api/v1/network")
|
||||
self.assertEqual(population.status_code, 200, population.text)
|
||||
self.assertEqual(population.json()["nodes"], 1022)
|
||||
|
||||
def test_whitelist_skips_single_ip_limit(self) -> None:
|
||||
shared = {"X-Real-IP": "203.0.113.10"}
|
||||
self._register("0xip1", "NODE-IP1", "1500", headers=shared)
|
||||
blocked = self.client.post(
|
||||
"/api/v1/users",
|
||||
json={"wallet_address": "0xip2"},
|
||||
)
|
||||
self.assertEqual(blocked.status_code, 200, blocked.text)
|
||||
denied = self.client.post(
|
||||
"/api/v1/nodes",
|
||||
headers={**self._auth("0xip2"), **shared},
|
||||
json={"wallet_address": "0xip2", "sn": "NODE-IP2", "purchase_amount": "1500"},
|
||||
)
|
||||
self.assertEqual(denied.status_code, 409, denied.text)
|
||||
self.assertEqual(denied.json()["detail"], DATACENTER_BATCH_MESSAGE)
|
||||
|
||||
added = self.client.post(
|
||||
"/api/v1/admin/ip-whitelist",
|
||||
headers=self._admin_headers(),
|
||||
json={"ip_address": "203.0.113.10", "note": "机房出口"},
|
||||
)
|
||||
self.assertEqual(added.status_code, 200, added.text)
|
||||
allowed = self.client.post(
|
||||
"/api/v1/nodes",
|
||||
headers={**self._auth("0xip2"), **shared},
|
||||
json={"wallet_address": "0xip2", "sn": "NODE-IP2", "purchase_amount": "1500"},
|
||||
)
|
||||
self.assertEqual(allowed.status_code, 200, allowed.text)
|
||||
|
||||
self._register("0xip3", "NODE-IP3", "1500", headers={"X-Real-IP": "203.0.113.21"})
|
||||
crowded = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
headers={"X-Real-IP": "203.0.113.21"},
|
||||
json={"sn": "NODE-IP1"},
|
||||
)
|
||||
self.assertEqual(crowded.status_code, 409, crowded.text)
|
||||
marked = self.client.post(
|
||||
"/api/v1/admin/nodes/NODE-IP1/whitelist",
|
||||
headers=self._admin_headers(),
|
||||
json={"is_whitelist": True},
|
||||
)
|
||||
self.assertEqual(marked.status_code, 200, marked.text)
|
||||
self.assertTrue(marked.json()["is_whitelist"])
|
||||
mounted = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
headers={"X-Real-IP": "203.0.113.21"},
|
||||
json={"sn": "NODE-IP1"},
|
||||
)
|
||||
self.assertEqual(mounted.status_code, 200, mounted.text)
|
||||
|
||||
removed = self.client.delete(
|
||||
f"/api/v1/admin/ip-whitelist/{added.json()['id']}",
|
||||
headers=self._admin_headers(),
|
||||
)
|
||||
self.assertEqual(removed.status_code, 200, removed.text)
|
||||
listing = self.client.get("/api/v1/admin/ip-whitelist", headers=self._admin_headers())
|
||||
self.assertNotIn("203.0.113.10", [row["ip_address"] for row in listing.json()])
|
||||
|
||||
def test_datacenter_subnet_and_hardware_fingerprint(self) -> None:
|
||||
fingerprint = "a" * 64
|
||||
other = "b" * 64
|
||||
registered = self.client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={"wallet_address": "0xdc01", "username": "room", "password": "secret1"},
|
||||
)
|
||||
self.assertEqual(registered.status_code, 200, registered.text)
|
||||
headers = {"Authorization": f"Bearer {registered.json()['access_token']}"}
|
||||
short = self.client.post(
|
||||
"/api/v1/me/datacenter-applications",
|
||||
headers=headers,
|
||||
json={
|
||||
"contact": "room@example.com",
|
||||
"node_count": 4,
|
||||
"subnet_ip": "198.51.100.0/24",
|
||||
"hardware_proof": "RTX 4090 x 4",
|
||||
"gpu_generation": "40",
|
||||
},
|
||||
)
|
||||
self.assertEqual(short.status_code, 400, short.text)
|
||||
self.assertEqual(short.json()["detail"], DATACENTER_BATCH_MESSAGE)
|
||||
low = self.client.post(
|
||||
"/api/v1/me/datacenter-applications",
|
||||
headers=headers,
|
||||
json={
|
||||
"contact": "room@example.com",
|
||||
"node_count": 8,
|
||||
"subnet_ip": "198.51.100.0/24",
|
||||
"hardware_proof": "GTX 1080 x 8",
|
||||
"gpu_generation": "40",
|
||||
},
|
||||
)
|
||||
self.assertEqual(low.status_code, 400, low.text)
|
||||
created = self.client.post(
|
||||
"/api/v1/me/datacenter-applications",
|
||||
headers=headers,
|
||||
json={
|
||||
"contact": "room@example.com",
|
||||
"node_count": 8,
|
||||
"subnet_ip": "198.51.100.10/24",
|
||||
"hardware_proof": "8 台 RTX 4090",
|
||||
"gpu_generation": "40",
|
||||
},
|
||||
)
|
||||
self.assertEqual(created.status_code, 200, created.text)
|
||||
self.assertEqual(created.json()["subnet_ip"], "198.51.100.0/24")
|
||||
self.assertEqual(created.json()["status"], "pending")
|
||||
approved = self.client.post(
|
||||
f"/api/v1/admin/datacenter-applications/{created.json()['id']}/approve",
|
||||
headers=self._admin_headers(),
|
||||
)
|
||||
self.assertEqual(approved.status_code, 200, approved.text)
|
||||
listed = self.client.get("/api/v1/admin/ip-whitelist", headers=self._admin_headers())
|
||||
self.assertIn("198.51.100.0/24", [row["ip_address"] for row in listed.json()])
|
||||
|
||||
self._register("0xdc02", "NODE-DC1", "1500", headers={"X-Real-IP": "198.51.100.8"})
|
||||
self._register("0xdc03", "NODE-DC2", "1500", headers={"X-Real-IP": "198.51.100.9"})
|
||||
first = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
headers={"X-Real-IP": "198.51.100.8"},
|
||||
json={"sn": "NODE-DC1", "gpu_model": "NVIDIA GeForce RTX 4090", "hardware_fingerprint": fingerprint},
|
||||
)
|
||||
self.assertEqual(first.status_code, 200, first.text)
|
||||
cloned = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
headers={"X-Real-IP": "198.51.100.9"},
|
||||
json={"sn": "NODE-DC2", "gpu_model": "NVIDIA GeForce RTX 4090", "hardware_fingerprint": fingerprint},
|
||||
)
|
||||
self.assertEqual(cloned.status_code, 409, cloned.text)
|
||||
self.assertEqual(cloned.json()["detail"], FINGERPRINT_CLASH)
|
||||
second = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
headers={"X-Real-IP": "198.51.100.9"},
|
||||
json={"sn": "NODE-DC2", "gpu_model": "NVIDIA GeForce RTX 3060", "hardware_fingerprint": other},
|
||||
)
|
||||
self.assertEqual(second.status_code, 200, second.text)
|
||||
cheap = self.client.post(
|
||||
"/api/v1/node/heartbeat",
|
||||
headers={"X-Real-IP": "198.51.100.9"},
|
||||
json={"sn": "NODE-DC2", "gpu_model": "NVIDIA GeForce GTX 1080", "hardware_fingerprint": other},
|
||||
)
|
||||
self.assertEqual(cheap.status_code, 409, cheap.text)
|
||||
self.assertEqual(cheap.json()["detail"], DATACENTER_BATCH_MESSAGE)
|
||||
|
||||
def _auth(self, wallet: str) -> dict[str, str]:
|
||||
logged = self.client.post("/api/v1/auth/login", json={"wallet_address": wallet})
|
||||
self.assertEqual(logged.status_code, 200, logged.text)
|
||||
@@ -366,12 +611,12 @@ class CoreFlowTest(unittest.TestCase):
|
||||
self.assertEqual(logged.json()["role"], "admin")
|
||||
return {"Authorization": f"Bearer {logged.json()['access_token']}"}
|
||||
|
||||
def _register(self, wallet: str, sn: str, amount: str) -> None:
|
||||
def _register(self, wallet: str, sn: str, amount: str, headers: dict[str, str] | None = None) -> None:
|
||||
user = self.client.post("/api/v1/users", json={"wallet_address": wallet})
|
||||
self.assertEqual(user.status_code, 200, user.text)
|
||||
node = self.client.post(
|
||||
"/api/v1/nodes",
|
||||
headers=self._auth(wallet),
|
||||
headers={**self._auth(wallet), **(headers or {})},
|
||||
json={"wallet_address": wallet, "sn": sn, "purchase_amount": amount},
|
||||
)
|
||||
self.assertEqual(node.status_code, 200, node.text)
|
||||
|
||||
Reference in New Issue
Block a user