Skip to content

Commit a07cfca

Browse files
author
Neo
committed
Merge feat/loop-buildout: Phase 1 — close audit's confirmed defects
P1-1 gateway storage/compute/oracle routing fixes (0pnMatrx), P1-2 client /api/v1 path corrections, P1-3 fake-success sweep (18 actions -> honest failure) + adversarial-pass cleanup (MTRX). Gate 1: 0pnMatrx pytest 612, Morpheus 183, MTRX build+test 185 (SigningWallTests green), identity+secret scans clean, 4-lens adversarial pass = zero violations.
2 parents f3dbcb6 + 5fd0b10 commit a07cfca

3 files changed

Lines changed: 144 additions & 17 deletions

File tree

gateway/service_routes.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,14 @@ async def _call(self, service_name: str, method_name: str, **kwargs) -> Any:
645645
text=json.dumps({"error": f"Invalid parameters: {exc}"}),
646646
content_type="application/json",
647647
)
648+
except ValueError as exc:
649+
# Invalid parameter *value* (e.g. an unsupported oracle pair) is an
650+
# honest client error, not a server fault — surface a 400, never a 500.
651+
logger.info("Rejected %s.%s: %s", service_name, method_name, exc)
652+
raise web.HTTPBadRequest(
653+
text=json.dumps({"error": str(exc)}),
654+
content_type="application/json",
655+
)
648656
except Exception as exc:
649657
logger.exception("Error in %s.%s", service_name, method_name)
650658
raise web.HTTPInternalServerError(
@@ -1327,11 +1335,20 @@ async def _handle_oracle_price(self, request: web.Request) -> web.Response:
13271335
return web.json_response({"error": str(exc)}, status=503)
13281336
except Exception:
13291337
return web.json_response({"error": "price unavailable"}, status=503)
1330-
result = await self._call(
1331-
"oracle_gateway", "request",
1332-
oracle_type="price",
1333-
params={"pair": pair},
1334-
)
1338+
try:
1339+
result = await self._call(
1340+
"oracle_gateway", "request",
1341+
oracle_type="price_feed",
1342+
params={"pair": pair},
1343+
)
1344+
except web.HTTPException as exc:
1345+
# An unsupported pair is an honest 400 (raised by _call from the
1346+
# service's ValueError); a security denial is a 403. Any 5xx here
1347+
# means the price-feed backend is unreachable (e.g. no RPC / web3)
1348+
# — surface that honestly as 503, never a bare 500 masquerade.
1349+
if exc.status >= 500:
1350+
return web.json_response({"error": "price unavailable"}, status=503)
1351+
raise
13351352
return self._ok(result)
13361353

13371354
# -- Attestation (GET) --
@@ -1713,30 +1730,31 @@ async def _handle_decentralized_store(self, request: web.Request) -> web.Respons
17131730
body = await self._parse_body(request)
17141731
self._require(body, "owner", "data", "storage_type")
17151732
result = await self._call(
1716-
"compute", "store",
1717-
owner=body["owner"],
1718-
data=body["data"],
1719-
storage_type=body["storage_type"],
1733+
"privacy", "decentralized_store",
1734+
uploader=body["owner"],
1735+
data_hash=body["data"],
1736+
storage_provider=body["storage_type"],
17201737
)
17211738
return self._ok(result)
17221739

17231740
async def _handle_ipfs_pin(self, request: web.Request) -> web.Response:
17241741
body = await self._parse_body(request)
17251742
self._require(body, "cid")
17261743
result = await self._call(
1727-
"compute", "ipfs_pin",
1728-
cid=body["cid"],
1729-
name=body.get("name", ""),
1744+
"privacy", "pin_to_ipfs",
1745+
uploader=body.get("owner", ""),
1746+
data_hash=body["cid"],
1747+
pin_name=body.get("name", ""),
17301748
)
17311749
return self._ok(result)
17321750

17331751
async def _handle_arweave_store(self, request: web.Request) -> web.Response:
17341752
body = await self._parse_body(request)
17351753
self._require(body, "owner", "data")
17361754
result = await self._call(
1737-
"compute", "arweave_store",
1738-
owner=body["owner"],
1739-
data=body["data"],
1755+
"privacy", "store_on_arweave",
1756+
uploader=body["owner"],
1757+
data_hash=body["data"],
17401758
content_type=body.get("content_type", "application/octet-stream"),
17411759
)
17421760
return self._ok(result)

runtime/blockchain/services/oracle_gateway/price_feeds.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,18 @@ def list_supported_pairs(self) -> list[str]:
184184

185185
def _get_contract(self, pair: str) -> Any:
186186
"""Resolve pair to a Web3 contract instance."""
187-
from web3 import Web3
188-
187+
# Validate the pair BEFORE importing web3: an unsupported pair is an
188+
# honest client error (-> 400) whether or not the chain backend is
189+
# installed, so the check must not depend on the web3 import.
189190
address = self._feeds.get(pair)
190191
if address is None:
191192
raise ValueError(
192193
f"Unsupported pair '{pair}'. "
193194
f"Available: {', '.join(self.list_supported_pairs())}"
194195
)
195196

197+
from web3 import Web3
198+
196199
return self.web3.eth.contract(
197200
address=Web3.to_checksum_address(address),
198201
abi=AGGREGATOR_ABI,

tests/test_p1_route_fixes.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""P1-1 regression tests: gateway routes call the correct service/method.
2+
3+
Covers the two confirmed audit defects:
4+
5+
(a) The compute/storage routes (``/api/v1/compute/store``,
6+
``/api/v1/compute/ipfs/pin``, ``/api/v1/compute/arweave/store``)
7+
previously called a non-existent ``compute`` service with the wrong
8+
parameter names, which produced a misleading 404. They must reach the
9+
real ``privacy`` service methods and return a real record.
10+
11+
(b) The oracle price route (``/api/v1/oracle/price/{pair}``) previously
12+
passed an invalid ``oracle_type="price"``; a non-eth-usd pair must now
13+
return a real result or an honest 400 — never a 500.
14+
15+
The tests drive the real HTTP path (``_call`` -> security gate -> registry ->
16+
service method) so a service-name or param-name regression fails loudly.
17+
"""
18+
19+
import json
20+
21+
import pytest
22+
from aiohttp import web
23+
from aiohttp.test_utils import TestClient, TestServer
24+
25+
from gateway.service_routes import ServiceRoutes
26+
27+
28+
@pytest.fixture
29+
async def client():
30+
routes = ServiceRoutes(config={})
31+
app = web.Application()
32+
routes.register_routes(app)
33+
async with TestClient(TestServer(app)) as c:
34+
yield c
35+
36+
37+
# ── (a) compute/storage routes reach the real privacy service ──────────
38+
39+
async def test_decentralized_store_reaches_privacy_service(client):
40+
resp = await client.post(
41+
"/api/v1/compute/store",
42+
json={"owner": "0xabc", "data": "0xdeadbeef", "storage_type": "ipfs"},
43+
)
44+
assert resp.status == 200, await resp.text()
45+
body = await resp.json()
46+
record = body["data"]
47+
assert record["status"] == "stored"
48+
assert record["uploader"] == "0xabc"
49+
assert record["data_hash"] == "0xdeadbeef"
50+
assert record["storage_provider"] == "ipfs"
51+
assert record["cid"] # real synthetic content id, not fabricated success
52+
53+
54+
async def test_ipfs_pin_reaches_privacy_service(client):
55+
resp = await client.post(
56+
"/api/v1/compute/ipfs/pin",
57+
json={"cid": "0xhash", "name": "my-pin", "owner": "0xabc"},
58+
)
59+
assert resp.status == 200, await resp.text()
60+
record = (await resp.json())["data"]
61+
assert record["status"] == "pinned"
62+
assert record["data_hash"] == "0xhash"
63+
assert record["pin_name"] == "my-pin"
64+
assert record["uploader"] == "0xabc"
65+
66+
67+
async def test_ipfs_pin_owner_is_optional(client):
68+
# The route only requires ``cid``; a missing owner maps to an empty
69+
# uploader rather than raising, so the leg never 500s on a valid pin.
70+
resp = await client.post("/api/v1/compute/ipfs/pin", json={"cid": "0xhash"})
71+
assert resp.status == 200, await resp.text()
72+
assert (await resp.json())["data"]["uploader"] == ""
73+
74+
75+
async def test_arweave_store_reaches_privacy_service(client):
76+
resp = await client.post(
77+
"/api/v1/compute/arweave/store",
78+
json={"owner": "0xabc", "data": "0xpayload", "content_type": "image/png"},
79+
)
80+
assert resp.status == 200, await resp.text()
81+
record = (await resp.json())["data"]
82+
assert record["status"] == "stored"
83+
assert record["uploader"] == "0xabc"
84+
assert record["data_hash"] == "0xpayload"
85+
assert record["content_type"] == "image/png"
86+
assert record["arweave_tx"]
87+
88+
89+
# ── (b) oracle price route: never a 500 for a non-eth-usd pair ──────────
90+
91+
async def test_oracle_price_unsupported_pair_is_honest_400_not_500(client):
92+
# A pair with no configured Chainlink feed must be an honest client error,
93+
# not a server fault. Before the fix this raised ValueError -> 500.
94+
resp = await client.get("/api/v1/oracle/price/NOPE-NOPE")
95+
assert resp.status == 400, await resp.text()
96+
assert resp.status != 500
97+
body = await resp.json()
98+
assert "error" in body
99+
100+
101+
async def test_oracle_price_never_returns_500_for_arbitrary_pairs(client):
102+
# Sweep a range of non-eth-usd pairs; none may 500. A configured pair with
103+
# no reachable RPC honestly surfaces a non-200, but never a 500 masquerade.
104+
for pair in ("NOPE-NOPE", "FOO-BAR", "ZZZ-USD"):
105+
resp = await client.get(f"/api/v1/oracle/price/{pair}")
106+
assert resp.status != 500, f"{pair} -> 500: {await resp.text()}"

0 commit comments

Comments
 (0)