@@ -1260,6 +1260,248 @@ async def handler(request: httpx.Request) -> httpx.Response:
12601260 assert stale_response .status_code == 401
12611261
12621262
1263+ def test_signed_upload_bridge_strict_rejects_unknown_hotkey () -> None :
1264+ registry = ChallengeRegistry ()
1265+ registry .create (ChallengeCreate (** _payload ()))
1266+ registry .set_status ("demo" , ChallengeStatus .ACTIVE )
1267+
1268+ class Cache :
1269+ def get (self ) -> dict [str , int ]:
1270+ return {"known" : 7 }
1271+
1272+ verifier = MinerUploadVerifier (
1273+ netuid = 42 ,
1274+ nonce_store = FakeNonceStore (),
1275+ metagraph_cache = Cache (), # type: ignore[arg-type]
1276+ now_fn = lambda : 1_000 ,
1277+ signature_verifier = lambda _hotkey , _message , signature : signature == "valid" ,
1278+ )
1279+
1280+ @asynccontextmanager
1281+ async def failing_client_factory ():
1282+ async def handler (request : httpx .Request ) -> httpx .Response :
1283+ raise httpx .ConnectError ("unexpected upstream call" , request = request )
1284+
1285+ transport = httpx .MockTransport (handler )
1286+ async with httpx .AsyncClient (transport = transport ) as client :
1287+ yield client
1288+
1289+ client = TestClient (
1290+ create_proxy_app (
1291+ registry = registry ,
1292+ client_factory = failing_client_factory ,
1293+ miner_verifier = verifier ,
1294+ challenge_token_provider = lambda _slug : "challenge-token" ,
1295+ )
1296+ )
1297+
1298+ response = client .post (
1299+ "/v1/challenges/demo/submissions" ,
1300+ content = b"zip-bytes" ,
1301+ headers = {
1302+ "X-Hotkey" : "unknown" ,
1303+ "X-Signature" : "valid" ,
1304+ "X-Nonce" : "nonce-strict-unknown" ,
1305+ "X-Timestamp" : "1000" ,
1306+ },
1307+ )
1308+
1309+ assert response .status_code == 401
1310+ assert response .json () == {"detail" : "unknown hotkey" }
1311+
1312+
1313+ def test_signed_upload_bridge_bypass_forwards_unknown_without_uid () -> None :
1314+ registry = ChallengeRegistry ()
1315+ registry .create (ChallengeCreate (** _payload ()))
1316+ registry .set_status ("demo" , ChallengeStatus .ACTIVE )
1317+ captured : dict [str , Any ] = {}
1318+
1319+ class Cache :
1320+ def get (self ) -> dict [str , int ]:
1321+ return {"known" : 7 }
1322+
1323+ challenge_app = FastAPI ()
1324+
1325+ @challenge_app .post ("/internal/v1/bridge/submissions" )
1326+ async def bridge (request : Request ) -> dict [str , object ]:
1327+ captured ["headers" ] = dict (request .headers )
1328+ captured ["body" ] = await request .body ()
1329+ return {"id" : "sub-bypass" , "status" : "pending" }
1330+
1331+ @asynccontextmanager
1332+ async def client_factory ():
1333+ transport = httpx .ASGITransport (app = challenge_app )
1334+ async with httpx .AsyncClient (
1335+ transport = transport , base_url = "http://challenge-demo:8000"
1336+ ) as client :
1337+ yield client
1338+
1339+ verifier = MinerUploadVerifier (
1340+ netuid = 42 ,
1341+ nonce_store = FakeNonceStore (),
1342+ metagraph_cache = Cache (), # type: ignore[arg-type]
1343+ now_fn = lambda : 1_000 ,
1344+ require_registered_hotkey = False ,
1345+ signature_verifier = lambda _hotkey , _message , signature : signature == "valid" ,
1346+ )
1347+ client = TestClient (
1348+ create_proxy_app (
1349+ registry = registry ,
1350+ client_factory = client_factory ,
1351+ miner_verifier = verifier ,
1352+ challenge_token_provider = lambda _slug : "challenge-token" ,
1353+ )
1354+ )
1355+
1356+ response = client .post (
1357+ "/v1/challenges/demo/submissions" ,
1358+ content = b"zip-bytes" ,
1359+ headers = {
1360+ "Content-Type" : "application/zip" ,
1361+ "X-Hotkey" : "unknown" ,
1362+ "X-Signature" : "valid" ,
1363+ "X-Nonce" : "nonce-bypass-unknown" ,
1364+ "X-Timestamp" : "1000" ,
1365+ "X-Platform-Verified-Uid" : "spoof" ,
1366+ },
1367+ )
1368+
1369+ assert response .status_code == 200
1370+ assert response .json () == {"id" : "sub-bypass" , "status" : "pending" }
1371+ headers = captured ["headers" ]
1372+ assert headers ["authorization" ] == "Bearer challenge-token"
1373+ assert headers ["x-platform-verified-hotkey" ] == "unknown"
1374+ assert headers ["x-platform-verified-nonce" ] == "nonce-bypass-unknown"
1375+ assert "x-platform-verified-uid" not in headers
1376+ assert captured ["body" ] == b"zip-bytes"
1377+
1378+
1379+ def test_signed_upload_bridge_disabled_registration_rejects_blocked_uid () -> None :
1380+ registry = ChallengeRegistry ()
1381+ registry .create (ChallengeCreate (** _payload ()))
1382+ registry .set_status ("demo" , ChallengeStatus .ACTIVE )
1383+
1384+ class Cache :
1385+ def get (self ) -> dict [str , int ]:
1386+ return {"known" : 0 }
1387+
1388+ verifier = MinerUploadVerifier (
1389+ netuid = 42 ,
1390+ nonce_store = FakeNonceStore (),
1391+ metagraph_cache = Cache (), # type: ignore[arg-type]
1392+ now_fn = lambda : 1_000 ,
1393+ require_registered_hotkey = False ,
1394+ signature_verifier = lambda _hotkey , _message , signature : signature == "valid" ,
1395+ )
1396+
1397+ @asynccontextmanager
1398+ async def failing_client_factory ():
1399+ async def handler (request : httpx .Request ) -> httpx .Response :
1400+ raise httpx .ConnectError ("unexpected upstream call" , request = request )
1401+
1402+ transport = httpx .MockTransport (handler )
1403+ async with httpx .AsyncClient (transport = transport ) as client :
1404+ yield client
1405+
1406+ client = TestClient (
1407+ create_proxy_app (
1408+ registry = registry ,
1409+ client_factory = failing_client_factory ,
1410+ miner_verifier = verifier ,
1411+ challenge_token_provider = lambda _slug : "challenge-token" ,
1412+ )
1413+ )
1414+
1415+ response = client .post (
1416+ "/v1/challenges/demo/submissions" ,
1417+ content = b"zip-bytes" ,
1418+ headers = {
1419+ "X-Hotkey" : "known" ,
1420+ "X-Signature" : "valid" ,
1421+ "X-Nonce" : "nonce-blocked" ,
1422+ "X-Timestamp" : "1000" ,
1423+ },
1424+ )
1425+
1426+ assert response .status_code == 401
1427+ assert response .json () == {"detail" : "blocked uid" }
1428+
1429+
1430+ def test_signed_upload_bridge_bypass_keeps_signature_time_replay_guards () -> None :
1431+ registry = ChallengeRegistry ()
1432+ registry .create (ChallengeCreate (** _payload ()))
1433+ registry .set_status ("demo" , ChallengeStatus .ACTIVE )
1434+
1435+ class Cache :
1436+ def get (self ) -> dict [str , int ]:
1437+ return {"known" : 7 }
1438+
1439+ verifier = MinerUploadVerifier (
1440+ netuid = 42 ,
1441+ nonce_store = FakeNonceStore (),
1442+ metagraph_cache = Cache (), # type: ignore[arg-type]
1443+ now_fn = lambda : 1_000 ,
1444+ require_registered_hotkey = False ,
1445+ signature_verifier = lambda _hotkey , _message , signature : signature == "valid" ,
1446+ )
1447+
1448+ @asynccontextmanager
1449+ async def failing_client_factory ():
1450+ async def handler (request : httpx .Request ) -> httpx .Response :
1451+ raise httpx .ConnectError ("boom" , request = request )
1452+
1453+ transport = httpx .MockTransport (handler )
1454+ async with httpx .AsyncClient (transport = transport ) as client :
1455+ yield client
1456+
1457+ client = TestClient (
1458+ create_proxy_app (
1459+ registry = registry ,
1460+ client_factory = failing_client_factory ,
1461+ miner_verifier = verifier ,
1462+ challenge_token_provider = lambda _slug : "challenge-token" ,
1463+ )
1464+ )
1465+ headers = {
1466+ "X-Hotkey" : "unknown" ,
1467+ "X-Signature" : "valid" ,
1468+ "X-Nonce" : "nonce-bypass-replay" ,
1469+ "X-Timestamp" : "1000" ,
1470+ }
1471+
1472+ invalid_signature = client .post (
1473+ "/v1/challenges/demo/submissions" ,
1474+ content = b"zip-bytes" ,
1475+ headers = {** headers , "X-Signature" : "invalid" , "X-Nonce" : "nonce-invalid" },
1476+ )
1477+ assert invalid_signature .status_code == 401
1478+ assert invalid_signature .json () == {"detail" : "invalid signature" }
1479+
1480+ stale = client .post (
1481+ "/v1/challenges/demo/submissions" ,
1482+ content = b"zip-bytes" ,
1483+ headers = {** headers , "X-Nonce" : "nonce-stale" , "X-Timestamp" : "1" },
1484+ )
1485+ assert stale .status_code == 401
1486+ assert stale .json () == {"detail" : "stale signature" }
1487+
1488+ first = client .post (
1489+ "/v1/challenges/demo/submissions" ,
1490+ content = b"zip-bytes" ,
1491+ headers = headers ,
1492+ )
1493+ assert first .status_code == 502
1494+ assert first .json () == {"detail" : "Challenge unavailable" }
1495+
1496+ replay = client .post (
1497+ "/v1/challenges/demo/submissions" ,
1498+ content = b"zip-bytes" ,
1499+ headers = headers ,
1500+ )
1501+ assert replay .status_code == 409
1502+ assert replay .json () == {"detail" : "nonce already used" }
1503+
1504+
12631505def test_production_admin_rejects_unsafe_challenge_image () -> None :
12641506 registry = ChallengeRegistry ()
12651507 client = TestClient (
0 commit comments