zerynex-python 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ """Fixed, bundled synthetic-only refund privacy MCP and preflight workflows.
2
+
3
+ No caller-controlled filesystem paths, arbitrary manifests, real wallet refs or
4
+ private PCZT data cross the agent trust boundary.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from importlib.resources import files
9
+ import json
10
+
11
+ from .client import ZerynexError
12
+ from .refund_privacy import audit_manifest, compare_manifests, RefundAuditInputError
13
+
14
+ SCENARIOS = ("isolated", "risky", "regression-expanded")
15
+ FIXTURE_FILES = {
16
+ "isolated": "refund-isolated.json",
17
+ "risky": "refund-risky.json",
18
+ "regression-expanded": "refund-regression-expanded.json",
19
+ }
20
+ LIMITATION = (
21
+ "Bundled synthetic and self-declared metadata only. No wallet, refund, viewing "
22
+ "keys, private information retrieval, quantum protection, or actual chain data."
23
+ )
24
+
25
+
26
+ def _load_scenario(scenario: str) -> dict[str, object]:
27
+ if type(scenario) is not str or scenario not in FIXTURE_FILES:
28
+ raise ZerynexError("Only reviewed synthetic refund scenarios are allowed.")
29
+ try:
30
+ raw = files("zerynex_py").joinpath("fixtures", FIXTURE_FILES[scenario]).read_bytes()
31
+ if len(raw) > 64 * 1024 or not raw:
32
+ raise ValueError("bad resource")
33
+ def unique(pairs):
34
+ result = {}
35
+ for key, value in pairs:
36
+ if key in result:
37
+ raise ValueError("duplicate")
38
+ result[key] = value
39
+ return result
40
+ value = json.loads(raw, object_pairs_hook=unique)
41
+ if type(value) is not dict:
42
+ raise ValueError("bad data")
43
+ return value
44
+ except (OSError, UnicodeError, ValueError, TypeError):
45
+ raise ZerynexError("Bundled synthetic refund scenario unavailable.") from None
46
+
47
+
48
+ def synthetic_refund_audit(scenario: str = "isolated") -> dict[str, object]:
49
+ """Return only a sanitized report on one of three known test scenarios."""
50
+ data = _load_scenario(scenario)
51
+ try:
52
+ report = audit_manifest(data)
53
+ except RefundAuditInputError:
54
+ raise ZerynexError("Bundled synthetic refund scenario failed validation.") from None
55
+ return {
56
+ "scenario": scenario,
57
+ "synthetic": True,
58
+ "report": report,
59
+ "limitation": LIMITATION,
60
+ }
61
+
62
+
63
+ def synthetic_refund_regression(
64
+ baseline: str = "risky", candidate: str = "regression-expanded"
65
+ ) -> dict[str, object]:
66
+ """Diff pre-reviewed scenarios, never expose flow tokens or arbitrary JSON."""
67
+ left, right = _load_scenario(baseline), _load_scenario(candidate)
68
+ try:
69
+ report = compare_manifests(left, right)
70
+ except RefundAuditInputError:
71
+ raise ZerynexError("Bundled synthetic refund comparison failed validation.") from None
72
+ return {
73
+ "baseline": baseline,
74
+ "candidate": candidate,
75
+ "synthetic": True,
76
+ "report": report,
77
+ "limitation": LIMITATION,
78
+ }
79
+
80
+
81
+ def synthetic_refund_smoke_suite() -> dict[str, object]:
82
+ """Verify the known pass/fail and per-flow risk-delta expectations."""
83
+ expected = {
84
+ "isolated": (True, 0),
85
+ "risky": (False, 5),
86
+ "regression-expanded": (False, 5),
87
+ }
88
+ outcomes: list[dict[str, object]] = []
89
+ for scenario, (passed, finding_count) in expected.items():
90
+ try:
91
+ data = synthetic_refund_audit(scenario)["report"]
92
+ matched = data["passed"] is passed and data["finding_count"] == finding_count
93
+ except (ZerynexError, KeyError, TypeError):
94
+ matched = False
95
+ outcomes.append({"scenario": scenario, "matched_expectation": matched})
96
+ for baseline, candidate, passed, new_events in (
97
+ ("isolated", "risky", False, 8),
98
+ ("risky", "regression-expanded", False, 1),
99
+ ("risky", "isolated", True, 0),
100
+ ):
101
+ try:
102
+ data = synthetic_refund_regression(baseline, candidate)["report"]
103
+ matched = data["passed"] is passed and data["new_risk_events"] == new_events
104
+ except (ZerynexError, KeyError, TypeError):
105
+ matched = False
106
+ outcomes.append({
107
+ "scenario": baseline + "-to-" + candidate,
108
+ "matched_expectation": matched,
109
+ })
110
+ count = sum(item["matched_expectation"] is True for item in outcomes)
111
+ return {
112
+ "synthetic": True,
113
+ "passed": count == len(outcomes),
114
+ "matched": count,
115
+ "total": len(outcomes),
116
+ "tests": outcomes,
117
+ "limitation": LIMITATION,
118
+ }
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """Offline, value-free OpenRPC method/parameter compatibility check."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import stat
10
+ import re
11
+ import sys
12
+
13
+ MAX_SCHEMA = 1024 * 1024
14
+ MAX_CONTRACT = 64 * 1024
15
+ SAFE_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_.-]{0,95}$")
16
+
17
+
18
+ class ContractInputError(Exception):
19
+ """Sanitized error, never copies JSON source values or private paths."""
20
+
21
+
22
+ def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict:
23
+ """Reject ambiguous JSON input with repeated keys at any depth."""
24
+ decoded = {}
25
+ for key, value in pairs:
26
+ if key in decoded:
27
+ raise ContractInputError("Duplicate JSON key.")
28
+ decoded[key] = value
29
+ return decoded
30
+
31
+
32
+ def unwrap_openrpc_schema(data: dict) -> dict:
33
+ """Accept only a bare OpenRPC document or saved rpc.discover JSON-RPC reply."""
34
+ if "jsonrpc" not in data:
35
+ return data
36
+ if (set(data) != {"jsonrpc", "id", "result"} or
37
+ data["jsonrpc"] != "2.0" or
38
+ (data["id"] is not None and type(data["id"]) not in (str, int)) or
39
+ not isinstance(data["result"], dict)):
40
+ raise ContractInputError("Invalid rpc.discover response.")
41
+ schema = data["result"]
42
+ if not isinstance(schema.get("openrpc"), str) or "jsonrpc" in schema:
43
+ raise ContractInputError("Expected OpenRPC schema result.")
44
+ return schema
45
+
46
+
47
+ def read_json(path: str, limit: int) -> dict:
48
+ try:
49
+ selected = Path(path)
50
+ if selected.is_symlink():
51
+ raise ContractInputError
52
+ # On POSIX, O_NOFOLLOW rejects symlink substitutions between our
53
+ # pathname check and open(). O_NONBLOCK avoids waiting on a swapped
54
+ # special file (e.g. FIFO); fstat checks the opened descriptor.
55
+ flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
56
+ fd = os.open(selected, flags)
57
+ with os.fdopen(fd, 'rb') as handle:
58
+ info = os.fstat(handle.fileno())
59
+ if not stat.S_ISREG(info.st_mode) or not 0 < info.st_size <= limit:
60
+ raise ContractInputError
61
+ raw = handle.read(limit + 1)
62
+ if len(raw) > limit:
63
+ raise ContractInputError
64
+ decoded = json.loads(raw, object_pairs_hook=reject_duplicate_keys)
65
+ if not isinstance(decoded, dict):
66
+ raise ContractInputError
67
+ return decoded
68
+ except (OSError, ValueError, UnicodeError, RecursionError, ContractInputError):
69
+ raise ContractInputError("Invalid or oversized local OpenRPC input.") from None
70
+
71
+
72
+ def parse_methods(data: dict, *, contract: bool) -> dict[str, list[tuple[str, bool]]]:
73
+ if contract and (type(data.get("version")) is not int or data["version"] != 1 or
74
+ set(data) != {"version", "methods"}):
75
+ raise ContractInputError("Unsupported RPC contract version.")
76
+ if not contract and not isinstance(data.get("openrpc"), str):
77
+ raise ContractInputError("OpenRPC version is required.")
78
+ items = data.get("methods")
79
+ if not isinstance(items, list) or not items or len(items) > 512:
80
+ raise ContractInputError("Invalid method list.")
81
+ parsed = {}
82
+ for entry in items:
83
+ if not isinstance(entry, dict) or not isinstance(entry.get("name"), str):
84
+ raise ContractInputError("Invalid method description.")
85
+ name = entry["name"]
86
+ if not SAFE_NAME.fullmatch(name) or name in parsed:
87
+ raise ContractInputError("Invalid or repeated method name.")
88
+ if contract and set(entry) != {"name", "params"}:
89
+ raise ContractInputError("Unexpected contract field.")
90
+ params = entry.get("params")
91
+ if not isinstance(params, list) or len(params) > 100:
92
+ raise ContractInputError("Invalid parameter list.")
93
+ found = set()
94
+ parsed_params = []
95
+ for param in params:
96
+ if not isinstance(param, dict):
97
+ raise ContractInputError("Invalid parameter declaration.")
98
+ pname = param.get("name")
99
+ required = param.get("required", False)
100
+ if (not isinstance(pname, str) or not SAFE_NAME.fullmatch(pname)
101
+ or pname in found or type(required) is not bool):
102
+ raise ContractInputError("Invalid or duplicate parameter declaration.")
103
+ if contract and set(param) != {"name", "required"}:
104
+ raise ContractInputError("Unexpected parameter contract field.")
105
+ found.add(pname)
106
+ parsed_params.append((pname, required))
107
+ parsed[name] = parsed_params
108
+ return parsed
109
+
110
+
111
+ def compare(schema: dict, contract: dict, *, deny_new_required: bool = False) -> dict:
112
+ actual = parse_methods(schema, contract=False)
113
+ expected = parse_methods(contract, contract=True)
114
+ findings = []
115
+ for method, expected_params in expected.items():
116
+ if method not in actual:
117
+ findings.append({"rule": "RPC-METHOD-MISSING", "method": method})
118
+ continue
119
+ actual_params = actual[method]
120
+ position = {name: index for index, (name, _) in enumerate(actual_params)}
121
+ last = -1
122
+ for name, required in expected_params:
123
+ if name not in position:
124
+ findings.append({"rule": "RPC-PARAMETER-MISSING", "method": method, "parameter": name})
125
+ continue
126
+ at = position[name]
127
+ if at < last:
128
+ findings.append({"rule": "RPC-PARAMETER-ORDER", "method": method, "parameter": name})
129
+ last = max(last, at)
130
+ if actual_params[at][1] != required:
131
+ findings.append({"rule": "RPC-REQUIRED-FLAG-CHANGED", "method": method, "parameter": name})
132
+ if deny_new_required:
133
+ expected_names = {name for name, _ in expected_params}
134
+ for name, required in actual_params:
135
+ if required and name not in expected_names:
136
+ findings.append({"rule": "RPC-NEW-REQUIRED-PARAMETER", "method": method, "parameter": name})
137
+ return {
138
+ "passed": len(findings) == 0,
139
+ "checked_methods": len(expected),
140
+ "observed_methods": len(actual),
141
+ "deny_new_required": deny_new_required,
142
+ "finding_count": len(findings),
143
+ "findings": findings,
144
+ "limitation": "Offline method and parameter structure only. No RPC execution, payload semantics, wallet safety or release compatibility verified.",
145
+ }
146
+
147
+
148
+ def main() -> int:
149
+ parser = argparse.ArgumentParser(description="Offline Zcash OpenRPC contract checker")
150
+ parser.add_argument("--schema", required=True, help="Local OpenRPC JSON file")
151
+ parser.add_argument("--contract", required=True, help="Expected method/parameter JSON file")
152
+ parser.add_argument("--deny-new-required", action="store_true", help="Report newly required parameters on contracted methods")
153
+ parser.add_argument("--json", action="store_true")
154
+ args = parser.parse_args()
155
+ try:
156
+ schema = unwrap_openrpc_schema(read_json(args.schema, MAX_SCHEMA))
157
+ report = compare(schema, read_json(args.contract, MAX_CONTRACT), deny_new_required=args.deny_new_required)
158
+ except ContractInputError:
159
+ print("Zerynex RPC: cannot evaluate input. Verify local schema and contract structure.", file=sys.stderr)
160
+ return 1
161
+ if args.json:
162
+ print(json.dumps(report, indent=2))
163
+ else:
164
+ print("Zerynex RPC: {} of {} contracted methods checked, {} finding(s).".format(
165
+ report["checked_methods"], report["checked_methods"], report["finding_count"]))
166
+ for item in report["findings"]:
167
+ print("{}: {} {}".format(item["rule"], item["method"], item.get("parameter", "")))
168
+ return 0 if report["passed"] else 2
169
+
170
+
171
+ if __name__ == "__main__":
172
+ sys.exit(main())
zerynex_py/rules.py ADDED
@@ -0,0 +1,55 @@
1
+ """Bounded, versioned public rule metadata; no transaction material or network."""
2
+ from __future__ import annotations
3
+ from importlib.resources import files
4
+ import json
5
+ import re
6
+ from .client import ZerynexError
7
+
8
+
9
+ def parse_rule_catalog(raw: str) -> dict[str, object]:
10
+ if not isinstance(raw,str) or not 0 < len(raw) < 32 * 1024:
11
+ raise ZerynexError('Invalid local rule catalog size.')
12
+ try:
13
+ def no_duplicates(pairs):
14
+ record={}
15
+ for key,value in pairs:
16
+ if key in record:
17
+ raise ValueError('duplicate keys')
18
+ record[key]=value
19
+ return record
20
+ data=json.loads(raw, object_pairs_hook=no_duplicates)
21
+ if (not isinstance(data,dict) or set(data) != {'schema_version','tool','rule_set_version','scope','rules'} or
22
+ type(data['schema_version']) is not int or data['schema_version'] != 1 or
23
+ data['tool'] != 'zerynex' or not isinstance(data['rule_set_version'],str) or
24
+ not re.fullmatch(r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}', data['rule_set_version']) or
25
+ not isinstance(data['scope'],str) or not 20 < len(data['scope']) <= 512):
26
+ raise ValueError
27
+ rules=data['rules']
28
+ if not isinstance(rules,list) or not 1 <= len(rules) <= 32:
29
+ raise ValueError
30
+ ids=[]
31
+ for rule in rules:
32
+ if (not isinstance(rule,dict) or set(rule) != {'id','title','severity','category','applies_to','summary'} or
33
+ not all(isinstance(rule[x],str) for x in ('id','title','severity','category','summary')) or
34
+ not re.fullmatch(r'PCZT-[A-Z-]{3,64}',rule['id']) or
35
+ not 5 <= len(rule['title']) <= 120 or not 10 <= len(rule['summary']) <= 256 or
36
+ rule['category'] not in ('metadata','pool_policy','handoff','regression') or
37
+ rule['severity'] not in ('warning','error') or
38
+ not isinstance(rule['applies_to'],list) or
39
+ rule['applies_to'] not in (['stage_comparison'],['analysis','stage_comparison'])):
40
+ raise ValueError
41
+ ids.append(rule['id'])
42
+ if ids != sorted(set(ids)) or any(not item.startswith('PCZT-') for item in ids):
43
+ raise ValueError
44
+ return data
45
+ except (ValueError,TypeError,KeyError):
46
+ raise ZerynexError('Invalid rule catalog from the local Rust engine.') from None
47
+
48
+
49
+ def bundled_rule_catalog() -> dict[str, object]:
50
+ """Read the packaged metadata snapshot (validated during release CI)."""
51
+ try:
52
+ raw=files('zerynex_py').joinpath('fixtures','rule-catalog.json').read_bytes()
53
+ return parse_rule_catalog(raw.decode('utf-8'))
54
+ except (OSError,UnicodeError,ValueError):
55
+ raise ZerynexError('Bundled rule catalog is unavailable.') from None
zerynex_py/z3.py ADDED
@@ -0,0 +1,107 @@
1
+ """Privacy-conscious local Z3 Zebra liveness/readiness checks.
2
+
3
+ Uses ONLY the documented public health routes on an explicitly loopback-only
4
+ endpoint. No arbitrary hostnames, DNS resolution, wallet JSON-RPC, auth cookies,
5
+ transaction data, node credentials, or external network calls.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, asdict
10
+ from http.client import HTTPConnection, HTTPException
11
+ import ipaddress
12
+ import json
13
+ from typing import Literal
14
+
15
+ from .client import ZerynexError
16
+
17
+ Status = Literal["healthy", "not_ready", "unavailable"]
18
+
19
+ @dataclass(frozen=True)
20
+ class ZebraHealthReport:
21
+ endpoint: str
22
+ liveness: Status
23
+ readiness: Status
24
+ summary: str
25
+ source: str = "Z3 Zebra documented /healthy and /ready endpoints"
26
+ caution: str = "This tests a local health endpoint only. It does not validate consensus, Zaino gRPC, or Zallet wallet functionality."
27
+
28
+ def as_dict(self) -> dict[str, str]:
29
+ return asdict(self)
30
+
31
+
32
+ def _safe_loopback_host(host: str) -> str:
33
+ if host == "localhost":
34
+ return "127.0.0.1" # Avoid OS proxy and non-loopback DNS surprises.
35
+ try:
36
+ addr = ipaddress.ip_address(host)
37
+ except ValueError:
38
+ raise ZerynexError("Only numeric loopback addresses or localhost are allowed.") from None
39
+ if not addr.is_loopback:
40
+ raise ZerynexError("Z3 diagnostics may connect to loopback only.")
41
+ return addr.compressed
42
+
43
+
44
+ def _query(host: str, port: int, route: str, timeout: float) -> int | None:
45
+ connection = HTTPConnection(host, port, timeout=timeout)
46
+ try:
47
+ connection.request("GET", route, headers={"Accept": "text/plain", "Connection": "close"})
48
+ response = connection.getresponse()
49
+ # A health response may contain implementation detail; discard the body
50
+ # rather than putting it into logs or an agent response.
51
+ response.read(4096)
52
+ return response.status
53
+ except (OSError, TimeoutError, HTTPException):
54
+ return None
55
+ finally:
56
+ connection.close()
57
+
58
+
59
+ def check_zebra_local(
60
+ *,
61
+ host: str = "127.0.0.1",
62
+ port: int = 8080,
63
+ timeout: float = 1.5,
64
+ ) -> ZebraHealthReport:
65
+ """Check Z3/Zebra local liveness and readiness; never scan public hosts."""
66
+ safe_host = _safe_loopback_host(host)
67
+ if type(port) is not int or not 1 <= port <= 65535:
68
+ raise ZerynexError("Invalid local Zebra health port.")
69
+ if not isinstance(timeout, (float, int)) or not (0 < timeout <= 3):
70
+ raise ZerynexError("Timeout must be greater than zero and at most three seconds.")
71
+ live = _query(safe_host, port, "/healthy", float(timeout))
72
+ ready = _query(safe_host, port, "/ready", float(timeout))
73
+
74
+ liveness: Status = "healthy" if live == 200 else "not_ready" if live is not None else "unavailable"
75
+ readiness: Status = "healthy" if ready == 200 else "not_ready" if ready is not None else "unavailable"
76
+ if live == 200 and ready == 200:
77
+ summary = "Zebra's local health and readiness endpoints both returned HTTP 200."
78
+ elif live == 200:
79
+ summary = "Zebra's local liveness endpoint responded, but readiness is not confirmed."
80
+ else:
81
+ summary = "Zebra liveness is not confirmed. Check the local Z3 services and port mapping."
82
+ return ZebraHealthReport(
83
+ endpoint=f"{safe_host}:{port}",
84
+ liveness=liveness,
85
+ readiness=readiness,
86
+ summary=summary,
87
+ )
88
+
89
+
90
+ def main() -> None:
91
+ import argparse
92
+ parser = argparse.ArgumentParser(description="Zerynex: local-only Z3 Zebra health diagnostics")
93
+ parser.add_argument("--host", default="127.0.0.1", help="localhost or numeric loopback only")
94
+ parser.add_argument("--port", type=int, default=8080, help="Z3_ZEBRA_HOST_HEALTH_PORT")
95
+ parser.add_argument("--timeout", type=float, default=1.5, help="Request timeout, at most three seconds")
96
+ args = parser.parse_args()
97
+ try:
98
+ result = check_zebra_local(host=args.host, port=args.port, timeout=args.timeout)
99
+ except ZerynexError:
100
+ parser.exit(1, "Invalid or disallowed local health probe configuration.\n")
101
+ print(json.dumps(result.as_dict(), indent=2))
102
+ if result.liveness != "healthy" or result.readiness != "healthy":
103
+ parser.exit(2)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """Offline policy check for locally supplied, release-labelled Zallet PCZT fixtures.
3
+
4
+ The manifest's producer attribution is an assertion by its author. This tool
5
+ cannot authenticate that a PCZT was produced by Zallet.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import base64
11
+ import hashlib
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ import re
16
+ import stat
17
+ import subprocess
18
+ import sys
19
+
20
+ MAX_MANIFEST = 16 * 1024
21
+ MAX_BASE64 = ((8 * 1024 * 1024 + 2) // 3) * 4
22
+ NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,79}\.b64\Z")
23
+ RULE = re.compile(r"[A-Z][A-Z0-9-]{0,79}\Z")
24
+ FIELD = re.compile(r"[a-z][a-z0-9_.-]{0,95}\Z")
25
+ DIGEST = re.compile(r"[0-9a-f]{64}\Z")
26
+ PINNED_RELEASE = "v0.1.0-beta.3"
27
+ PINNED_COMMIT = "987382f67e622915228686e9f956c6a9c9a7514c"
28
+
29
+
30
+ class FixtureError(Exception):
31
+ """Never include fixture content or filesystem paths in this error."""
32
+
33
+
34
+ def unique_pairs(pairs):
35
+ result = {}
36
+ for key, value in pairs:
37
+ if key in result:
38
+ raise FixtureError("Ambiguous manifest JSON.")
39
+ result[key] = value
40
+ return result
41
+
42
+
43
+ def read_regular(path: Path, limit: int) -> bytes:
44
+ try:
45
+ flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
46
+ fd = os.open(path, flags)
47
+ with os.fdopen(fd, "rb") as handle:
48
+ info = os.fstat(handle.fileno())
49
+ if not stat.S_ISREG(info.st_mode) or not 0 < info.st_size <= limit:
50
+ raise FixtureError("Invalid local fixture size or type.")
51
+ value = handle.read(limit + 1)
52
+ if not 0 < len(value) <= limit:
53
+ raise FixtureError("Invalid local fixture size or type.")
54
+ return value
55
+ except (OSError, ValueError):
56
+ raise FixtureError("Cannot read a regular local fixture.") from None
57
+
58
+
59
+ def validate_manifest(value: object) -> list[dict]:
60
+ if not isinstance(value, dict) or set(value) != {"version", "producer", "cases"} or type(value["version"]) is not int or value["version"] != 1:
61
+ raise FixtureError("Invalid fixture manifest.")
62
+ producer = value["producer"]
63
+ if (not isinstance(producer, dict) or set(producer) != {"release", "commit", "origin"}
64
+ or producer != {"release": PINNED_RELEASE, "commit": PINNED_COMMIT,
65
+ "origin": "locally-supplied-zallet-output"}):
66
+ raise FixtureError("Unsupported or unpinned producer assertion.")
67
+ cases = value["cases"]
68
+ if not isinstance(cases, list) or not 1 <= len(cases) <= 16:
69
+ raise FixtureError("Invalid fixture case count.")
70
+ names = set()
71
+ for case in cases:
72
+ if not isinstance(case, dict) or set(case) != {"file", "sha256", "deny_transparent", "expect"}:
73
+ raise FixtureError("Invalid fixture case.")
74
+ name = case["file"]
75
+ expect = case["expect"]
76
+ if (not isinstance(name, str) or not NAME.fullmatch(name) or name in names
77
+ or not isinstance(case["sha256"], str) or not DIGEST.fullmatch(case["sha256"])
78
+ or type(case["deny_transparent"]) is not bool
79
+ or not isinstance(expect, list) or len(expect) > 32):
80
+ raise FixtureError("Invalid fixture case.")
81
+ names.add(name)
82
+ seen = set()
83
+ for item in expect:
84
+ if (not isinstance(item, dict) or set(item) != {"rule", "field"}
85
+ or not isinstance(item["rule"], str) or not RULE.fullmatch(item["rule"])
86
+ or not isinstance(item["field"], str) or not FIELD.fullmatch(item["field"])):
87
+ raise FixtureError("Invalid finding expectation.")
88
+ pair = (item["rule"], item["field"])
89
+ if pair in seen:
90
+ raise FixtureError("Repeated finding expectation.")
91
+ seen.add(pair)
92
+ return cases
93
+
94
+
95
+ def run(manifest_path: Path, cli: Path) -> dict:
96
+ try:
97
+ manifest = json.loads(read_regular(manifest_path, MAX_MANIFEST), object_pairs_hook=unique_pairs)
98
+ except (ValueError, UnicodeError, RecursionError):
99
+ raise FixtureError("Invalid fixture manifest JSON.") from None
100
+ cases = validate_manifest(manifest)
101
+ results = []
102
+ for case in cases:
103
+ encoded = read_regular(manifest_path.parent / case["file"], MAX_BASE64)
104
+ if hashlib.sha256(encoded).hexdigest() != case["sha256"]:
105
+ raise FixtureError("Fixture digest mismatch.")
106
+ try:
107
+ decoded = base64.b64decode(encoded, validate=True)
108
+ if not decoded or len(decoded) > 8 * 1024 * 1024 or base64.b64encode(decoded) != encoded:
109
+ raise FixtureError("Invalid canonical base64 fixture.")
110
+ except (ValueError, base64.binascii.Error):
111
+ raise FixtureError("Invalid canonical base64 fixture.") from None
112
+ args = [str(cli), "-", "--input-format", "base64", "--json"]
113
+ if case["deny_transparent"]:
114
+ args.append("--deny-transparent")
115
+ try:
116
+ completed = subprocess.run(args, input=encoded, capture_output=True, timeout=30, check=False)
117
+ except (OSError, subprocess.TimeoutExpired):
118
+ raise FixtureError("Local analyzer unavailable or timed out.") from None
119
+ if completed.returncode not in (0, 2) or len(completed.stdout) > 1024 * 1024:
120
+ raise FixtureError("Local analyzer rejected a fixture.")
121
+ try:
122
+ report = json.loads(completed.stdout, object_pairs_hook=unique_pairs)
123
+ findings = report["findings"]
124
+ actual = sorted((item["rule"], item["field"]) for item in findings)
125
+ if (not isinstance(findings, list) or len(findings) > 32
126
+ or type(report["deny_transparent"]) is not bool
127
+ or report["deny_transparent"] != case["deny_transparent"]
128
+ or (completed.returncode == 0) != (len(actual) == 0)
129
+ or any(not RULE.fullmatch(rule) or not FIELD.fullmatch(field)
130
+ for rule, field in actual)):
131
+ raise FixtureError("Invalid analyzer report.")
132
+ except (ValueError, KeyError, TypeError, AttributeError):
133
+ raise FixtureError("Invalid analyzer report.") from None
134
+ expected = sorted((item["rule"], item["field"]) for item in case["expect"])
135
+ results.append({"case": len(results) + 1, "matched": actual == expected,
136
+ "finding_count": len(actual), "rules": sorted(set(rule for rule, _ in actual))})
137
+ return {"evidence": "locally-supplied-fixture; producer origin asserted, not authenticated",
138
+ "release_assertion": PINNED_RELEASE, "case_count": len(results),
139
+ "passed": all(item["matched"] for item in results), "cases": results,
140
+ "limitation": "No wallet RPC, signing, proving, broadcast, or live Zallet interoperability verified."}
141
+
142
+
143
+ def main() -> int:
144
+ parser = argparse.ArgumentParser(description=__doc__)
145
+ parser.add_argument("--manifest", required=True, type=Path)
146
+ parser.add_argument("--cli", required=True, type=Path)
147
+ args = parser.parse_args()
148
+ try:
149
+ result = run(args.manifest, args.cli)
150
+ except FixtureError:
151
+ print("Zerynex: invalid local PCZT fixture or analyzer result.", file=sys.stderr)
152
+ return 1
153
+ print(json.dumps(result, indent=2))
154
+ return 0 if result["passed"] else 2
155
+
156
+
157
+ if __name__ == "__main__":
158
+ raise SystemExit(main())